[
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: gradle\n  directory: \"/\"\n  schedule:\n    interval: daily\n  open-pull-requests-limit: 10\n  ignore:\n  - dependency-name: androidx.camera:camera-lifecycle\n    versions:\n    - 1.1.0-alpha04\n  - dependency-name: androidx.camera:camera-camera2\n    versions:\n    - 1.1.0-alpha04\n  - dependency-name: androidx.camera:camera-view\n    versions:\n    - 1.0.0-alpha24\n  - dependency-name: androidx.camera:camera-core\n    versions:\n    - 1.1.0-alpha04\n  - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-android\n    versions:\n    - 1.4.3-native-mt\n  - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-core\n    versions:\n    - 1.4.3-native-mt\n  - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-test\n    versions:\n    - 1.4.3-native-mt\n  - dependency-name: org.jetbrains.kotlin:kotlin-test\n    versions:\n    - 1.4.31\n  - dependency-name: org.jetbrains.kotlin.plugin.serialization\n    versions:\n    - 1.4.30\n    - 1.4.31\n  - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin\n    versions:\n    - 1.4.30\n    - 1.4.31\n"
  },
  {
    "path": ".github/workflows/android_test.yml",
    "content": "name: Instrumentation Tests\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  instrumentation-test:\n\n    runs-on: macOS-latest\n\n    steps:\n      - name: checkout\n        uses: actions/checkout@v2\n\n      - name: test\n        uses: reactivecircus/android-emulator-runner@v2\n        with:\n          api-level: 29\n          script: ./gradlew connectedCheck\n\n      - name: upload-artifacts\n        uses: actions/upload-artifact@v2\n        if: failure()\n        with:\n          name: test-report\n          path: ${{ github.workspace }}/*/build/reports/\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  lint:\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: checkout\n        uses: actions/checkout@v2\n\n      - name: set up JDK 1.8\n        uses: actions/setup-java@v1\n        with:\n          java-version: 1.8\n\n      - name: lint\n        run: ./gradlew ktlint\n\n      - name: CheckStyle\n        run: ./gradlew checkJavaStyle\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  release:\n    types: [published]\n\njobs:\n  run_final_checks:\n    name: Run final checks\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: checkout\n        uses: actions/checkout@v2\n        with:\n          ref: ${{ github.event.release.target_commitish }}\n          token: ${{secrets.SERVICE_PERSONAL_ACCESS_TOKEN}}\n          submodules: recursive\n\n      - name: set up JDK 1.8\n        uses: actions/setup-java@v1\n        with:\n          java-version: 1.8\n\n      - name: lint\n        run: ./gradlew ktlint\n\n      - name: CheckStyle\n        run: ./gradlew checkJavaStyle\n\n      - name: test\n        run: ./gradlew test\n\n  finalize_release:\n    needs: run_final_checks\n    name: Finalize Release\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: checkout\n        uses: actions/checkout@v2\n        with:\n          ref: ${{ github.event.release.target_commitish }}\n          token: ${{secrets.SERVICE_PERSONAL_ACCESS_TOKEN}}\n          submodules: recursive\n\n      - name: get current tag\n        id: get_tag\n        run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}\n\n      - name: update version\n        env:\n          TAG_VERSION: ${{ steps.get_tag.outputs.VERSION }}\n        run: |\n          truncate -s $(( $(stat -c \"%s\" gradle.properties) - $(tail -n 1 gradle.properties | wc -c) )) gradle.properties\n          echo \"version=$TAG_VERSION\" >> gradle.properties\n          cat gradle.properties\n\n      - name: generate changelog\n        uses: heinrichreimer/github-changelog-generator-action@v2.1.1\n        with:\n          user: \"getbouncer\"\n          project: \"cardscan-android\"\n          repo: \"getbouncer/cardscan-android\"\n          token: ${{ secrets.SERVICE_PERSONAL_ACCESS_TOKEN }}\n          sinceTag: \"1.0.5151\"\n          pullRequests: \"true\"\n          prWoLabels: \"true\"\n          issues: \"true\"\n          issuesWoLabels: \"true\"\n          author: \"true\"\n          base: \"HISTORY.md\"\n          unreleased: \"true\"\n          breakingLabels: \"Versioning - BREAKING\"\n          enhancementLabels: \"Type - Enhancement, Type - Feature\"\n          bugLabels: \"Type - Fix, Bug - Fixed\"\n          deprecatedLabels: \"Type - Deprecated\"\n          removedLabels: \"Type - Removal\"\n          securityLabels: \"Security Fix\"\n          excludeLabels: \"Skip-Changelog\"\n\n      - name: create commit\n        id: commit\n        uses: stefanzweifel/git-auto-commit-action@v4\n        with:\n          branch: ${{ github.event.release.target_commitish }}\n          commit_message: \"Automatic changelog update\"\n          file_pattern: \"gradle.properties CHANGELOG.md\"\n\n  publish:\n    needs: finalize_release\n    name: Publish to MavenCentral\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: checkout\n        uses: actions/checkout@v2\n        with:\n          ref: ${{ github.event.release.target_commitish }}\n          token: ${{secrets.SERVICE_PERSONAL_ACCESS_TOKEN}}\n          submodules: recursive\n\n      - name: set up JDK 1.8\n        uses: actions/setup-java@v1\n        with:\n          java-version: 1.8\n\n      # Base64 decodes and pipes the GPG key content into the secret file\n      - name: Prepare environment\n        env:\n          GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY }}\n          SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}\n        run: |\n          git fetch --unshallow\n          sudo bash -c \"echo '$GPG_KEY_CONTENTS' | base64 -d > '$SIGNING_SECRET_KEY_RING_FILE'\"\n\n      - name: Build release\n        run: ./gradlew assembleRelease\n\n      - name: Source jar and dokka\n        run: ./gradlew androidSourcesJar javadocJar\n\n      - name: Publish to MavenCentral\n        env:\n          OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}\n          OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}\n          SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}\n          SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}\n          SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}\n          SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }}\n        run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository\n"
  },
  {
    "path": ".github/workflows/unit_test.yml",
    "content": "name: Unit Tests\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  test:\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: checkout\n        uses: actions/checkout@v2\n\n      - name: set up JDK 1.8\n        uses: actions/setup-java@v1\n        with:\n          java-version: 1.8\n\n      - name: test\n        run: ./gradlew test\n\n      - name: upload-artifacts\n        uses: actions/upload-artifact@v2\n        if: failure()\n        with:\n          name: test-report\n          path: ${{ github.workspace }}/*/build/reports/\n"
  },
  {
    "path": ".github_changelog_generator",
    "content": "since-tag=2.0.0015\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea/caches\n/.idea/compiler.xml\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor.xml\n/.idea/assetWizardSettings.xml\n/.idea/encodings.xml\n/.idea/gradle.xml\n/.idea/misc.xml\n/.idea/runConfigurations.xml\n/.idea/vcs.xml\n/.idea/dictionaries\n/.idea/codeStyles/Project.xml\n/.idea/codeStyles/codeStyleConfig.xml\n/.idea/.name\n/.idea/checkstyle-idea.xml\n/.idea/jarRepositories.xml\n.DS_Store\n/build\n/captures\n.externalNativeBuild\nlangapiconfig.json\n*.cxx\ngithub_token\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [2.2.0003](https://github.com/getbouncer/cardscan-android/tree/2.2.0003) (2022-06-15)\n\n**Closed issues:**\n\n- Crash when intent is null in parseResult callback [\\#506](https://github.com/getbouncer/cardscan-android/issues/506)\n\n**Merged pull requests:**\n\n- Add deprecation notice to cardscan-android [\\#523](https://github.com/getbouncer/cardscan-android/pull/523) ([awush-stripe](https://github.com/awush-stripe))\n- Remove references to bouncer emails [\\#508](https://github.com/getbouncer/cardscan-android/pull/508) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.2.0002](https://github.com/getbouncer/cardscan-android/tree/2.2.0002) (2022-04-11)\n\n**Closed issues:**\n\n- failed to build in android studio bumblebee [\\#494](https://github.com/getbouncer/cardscan-android/issues/494)\n\n**Merged pull requests:**\n\n- Prevent crash on null result intent [\\#507](https://github.com/getbouncer/cardscan-android/pull/507) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.2.0001](https://github.com/getbouncer/cardscan-android/tree/2.2.0001) (2022-03-28)\n\n**Closed issues:**\n\n- The new CardScanSheet API is not testable [\\#493](https://github.com/getbouncer/cardscan-android/issues/493)\n- The new CardScanSheet API is not testable [\\#492](https://github.com/getbouncer/cardscan-android/issues/492)\n- The new CardScanSheet API doesn't deliver result when starter activity is destroyed [\\#491](https://github.com/getbouncer/cardscan-android/issues/491)\n- Getbouncer console not accessible. [\\#487](https://github.com/getbouncer/cardscan-android/issues/487)\n\n**Merged pull requests:**\n\n- Update the interface so reference to the callback is not lost on activity killed [\\#499](https://github.com/getbouncer/cardscan-android/pull/499) ([awush-stripe](https://github.com/awush-stripe))\n- Make the activity result registry testable [\\#498](https://github.com/getbouncer/cardscan-android/pull/498) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0023](https://github.com/getbouncer/cardscan-android/tree/2.1.0023) (2022-01-18)\n\n## [2.1.0022](https://github.com/getbouncer/cardscan-android/tree/2.1.0022) (2022-01-17)\n\n## [2.1.0021](https://github.com/getbouncer/cardscan-android/tree/2.1.0021) (2022-01-06)\n\n**Merged pull requests:**\n\n- Simplify the API interface for CardScan [\\#482](https://github.com/getbouncer/cardscan-android/pull/482) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0020](https://github.com/getbouncer/cardscan-android/tree/2.1.0020) (2021-12-21)\n\n**Merged pull requests:**\n\n- Correctly report final stats [\\#475](https://github.com/getbouncer/cardscan-android/pull/475) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0019](https://github.com/getbouncer/cardscan-android/tree/2.1.0019) (2021-12-09)\n\n## [2.1.0018](https://github.com/getbouncer/cardscan-android/tree/2.1.0018) (2021-10-20)\n\n## [2.1.0017](https://github.com/getbouncer/cardscan-android/tree/2.1.0017) (2021-10-20)\n\n**Merged pull requests:**\n\n- Revert model storage file name [\\#461](https://github.com/getbouncer/cardscan-android/pull/461) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0016](https://github.com/getbouncer/cardscan-android/tree/2.1.0016) (2021-10-04)\n\n## [2.1.0015](https://github.com/getbouncer/cardscan-android/tree/2.1.0015) (2021-10-04)\n\n**Merged pull requests:**\n\n- Make prepareScan more accessible by using a callback [\\#458](https://github.com/getbouncer/cardscan-android/pull/458) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0014](https://github.com/getbouncer/cardscan-android/tree/2.1.0014) (2021-09-24)\n\n**Merged pull requests:**\n\n- Fix memory leak and double camera unbind [\\#456](https://github.com/getbouncer/cardscan-android/pull/456) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0014-alpha02](https://github.com/getbouncer/cardscan-android/tree/2.1.0014-alpha02) (2021-09-23)\n\n## [2.1.0014-alpha01](https://github.com/getbouncer/cardscan-android/tree/2.1.0014-alpha01) (2021-09-17)\n\n## [2.1.0014-downgrade-core-ktx01](https://github.com/getbouncer/cardscan-android/tree/2.1.0014-downgrade-core-ktx01) (2021-09-17)\n\n## [2.1.0013](https://github.com/getbouncer/cardscan-android/tree/2.1.0013) (2021-09-17)\n\n**Closed issues:**\n\n- java.lang.RuntimeException: getParameters failed \\(empty parameters\\) [\\#448](https://github.com/getbouncer/cardscan-android/issues/448)\n\n**Merged pull requests:**\n\n- Add stat tracking fetcher [\\#451](https://github.com/getbouncer/cardscan-android/pull/451) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0012](https://github.com/getbouncer/cardscan-android/tree/2.1.0012) (2021-09-15)\n\n**Merged pull requests:**\n\n- Fix crash for Redmi Note 9 when camera closes prematurely [\\#449](https://github.com/getbouncer/cardscan-android/pull/449) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0011](https://github.com/getbouncer/cardscan-android/tree/2.1.0011) (2021-09-08)\n\n**Merged pull requests:**\n\n- Destory created renderscript types [\\#447](https://github.com/getbouncer/cardscan-android/pull/447) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0010](https://github.com/getbouncer/cardscan-android/tree/2.1.0010) (2021-09-01)\n\n**Merged pull requests:**\n\n- Allow ranges of dependencies [\\#442](https://github.com/getbouncer/cardscan-android/pull/442) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0009](https://github.com/getbouncer/cardscan-android/tree/2.1.0009) (2021-07-29)\n\n**Closed issues:**\n\n- \"Cannot add a null child view to a ViewGroup\" Exception on Camera1Adapter [\\#435](https://github.com/getbouncer/cardscan-android/issues/435)\n- CardScanActivity vs CardScanFlow warmUp/prepareScan [\\#434](https://github.com/getbouncer/cardscan-android/issues/434)\n- Are Tensorflow/lite vulnerabilities a concern? [\\#427](https://github.com/getbouncer/cardscan-android/issues/427)\n- Expiry date overlay doesn't show [\\#406](https://github.com/getbouncer/cardscan-android/issues/406)\n\n**Merged pull requests:**\n\n- Fix camera preview null crash and restrict CardScanFlow to library access [\\#436](https://github.com/getbouncer/cardscan-android/pull/436) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0008](https://github.com/getbouncer/cardscan-android/tree/2.1.0008) (2021-07-19)\n\n**Merged pull requests:**\n\n- Simplify onScanReady [\\#426](https://github.com/getbouncer/cardscan-android/pull/426) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0007](https://github.com/getbouncer/cardscan-android/tree/2.1.0007) (2021-07-15)\n\n**Merged pull requests:**\n\n- Downgrade kotlin gradle plugin [\\#422](https://github.com/getbouncer/cardscan-android/pull/422) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0006](https://github.com/getbouncer/cardscan-android/tree/2.1.0006) (2021-07-15)\n\n**Merged pull requests:**\n\n- Downgrade kotlin methods in loop [\\#421](https://github.com/getbouncer/cardscan-android/pull/421) ([awush-stripe](https://github.com/awush-stripe))\n- Downgrade kotlin libraries to 1.4.3 [\\#420](https://github.com/getbouncer/cardscan-android/pull/420) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0005-ktx1.4.3](https://github.com/getbouncer/cardscan-android/tree/2.1.0005-ktx1.4.3) (2021-07-15)\n\n## [2.1.0005](https://github.com/getbouncer/cardscan-android/tree/2.1.0005) (2021-07-14)\n\n**Merged pull requests:**\n\n- Update dependencies [\\#419](https://github.com/getbouncer/cardscan-android/pull/419) ([awush-stripe](https://github.com/awush-stripe))\n- Update coroutines [\\#418](https://github.com/getbouncer/cardscan-android/pull/418) ([awush-stripe](https://github.com/awush-stripe))\n- Bump kotlin-test from 1.5.10 to 1.5.21 [\\#417](https://github.com/getbouncer/cardscan-android/pull/417) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump org.jetbrains.kotlin.plugin.serialization from 1.5.10 to 1.5.21 [\\#416](https://github.com/getbouncer/cardscan-android/pull/416) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump kotlin-gradle-plugin from 1.5.10 to 1.5.21 [\\#415](https://github.com/getbouncer/cardscan-android/pull/415) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump junit from 1.1.2 to 1.1.3 [\\#413](https://github.com/getbouncer/cardscan-android/pull/413) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump org.jetbrains.dokka from 1.4.32 to 1.5.0 [\\#411](https://github.com/getbouncer/cardscan-android/pull/411) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump kotlinx-serialization-json from 1.2.1 to 1.2.2 [\\#409](https://github.com/getbouncer/cardscan-android/pull/409) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump camera-view from 1.0.0-alpha25 to 1.0.0-alpha26 [\\#405](https://github.com/getbouncer/cardscan-android/pull/405) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump espresso-core from 3.3.0 to 3.4.0 [\\#404](https://github.com/getbouncer/cardscan-android/pull/404) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump runner from 1.3.0 to 1.4.0 [\\#403](https://github.com/getbouncer/cardscan-android/pull/403) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump core from 1.3.0 to 1.4.0 [\\#402](https://github.com/getbouncer/cardscan-android/pull/402) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump gradle from 4.2.1 to 4.2.2 [\\#401](https://github.com/getbouncer/cardscan-android/pull/401) ([dependabot[bot]](https://github.com/apps/dependabot))\n\n## [2.1.0004](https://github.com/getbouncer/cardscan-android/tree/2.1.0004) (2021-07-12)\n\n**Closed issues:**\n\n- Missing dependency on Maven Central updating to 2.1.0003 [\\#407](https://github.com/getbouncer/cardscan-android/issues/407)\n\n**Merged pull requests:**\n\n- Fix deployment names for scan-payment-full and scan-payment-minimal. [\\#408](https://github.com/getbouncer/cardscan-android/pull/408) ([awush-stripe](https://github.com/awush-stripe))\n- Bump core-ktx from 1.5.0 to 1.6.0 [\\#400](https://github.com/getbouncer/cardscan-android/pull/400) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Update card images used for testing [\\#396](https://github.com/getbouncer/cardscan-android/pull/396) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0003](https://github.com/getbouncer/cardscan-android/tree/2.1.0003) (2021-06-18)\n\n**Merged pull requests:**\n\n- Cycle beta versions faster [\\#395](https://github.com/getbouncer/cardscan-android/pull/395) ([awush-stripe](https://github.com/awush-stripe))\n- Bump fragment-ktx from 1.3.4 to 1.3.5 [\\#394](https://github.com/getbouncer/cardscan-android/pull/394) ([dependabot[bot]](https://github.com/apps/dependabot))\n\n## [2.1.0002](https://github.com/getbouncer/cardscan-android/tree/2.1.0002) (2021-06-11)\n\n**Closed issues:**\n\n- Some card scans reversed [\\#388](https://github.com/getbouncer/cardscan-android/issues/388)\n\n**Merged pull requests:**\n\n- Make scan ready checks JVM static [\\#393](https://github.com/getbouncer/cardscan-android/pull/393) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.1.0001](https://github.com/getbouncer/cardscan-android/tree/2.1.0001) (2021-06-11)\n\n**Merged pull requests:**\n\n- Add QR false positive detection check [\\#392](https://github.com/getbouncer/cardscan-android/pull/392) ([awush-stripe](https://github.com/awush-stripe))\n- Add check to determine if the scan is ready [\\#391](https://github.com/getbouncer/cardscan-android/pull/391) ([awush-stripe](https://github.com/awush-stripe))\n- Default to download OCR / UX models [\\#390](https://github.com/getbouncer/cardscan-android/pull/390) ([awush-stripe](https://github.com/awush-stripe))\n- Upgrade dependencies [\\#389](https://github.com/getbouncer/cardscan-android/pull/389) ([awush-stripe](https://github.com/awush-stripe))\n\n## [2.0.0090](https://github.com/getbouncer/cardscan-android/tree/2.0.0090) (2021-05-24)\n\n## [2.0.0089](https://github.com/getbouncer/cardscan-android/tree/2.0.0089) (2021-05-24)\n\n**Merged pull requests:**\n\n- Use button margin dimensions [\\#381](https://github.com/getbouncer/cardscan-android/pull/381) ([awush-stripe](https://github.com/awush-stripe))\n- Bump appcompat from 1.2.0 to 1.3.0 [\\#380](https://github.com/getbouncer/cardscan-android/pull/380) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump core-ktx from 1.3.2 to 1.5.0 [\\#379](https://github.com/getbouncer/cardscan-android/pull/379) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump fragment-ktx from 1.3.3 to 1.3.4 [\\#378](https://github.com/getbouncer/cardscan-android/pull/378) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump kotlinx-serialization-json from 1.2.0 to 1.2.1 [\\#372](https://github.com/getbouncer/cardscan-android/pull/372) ([dependabot[bot]](https://github.com/apps/dependabot))\n\n## [2.0.0088](https://github.com/getbouncer/cardscan-android/tree/2.0.0088) (2021-05-18)\n\n**Merged pull requests:**\n\n- Improve error handling around cache calculation [\\#377](https://github.com/getbouncer/cardscan-android/pull/377) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0087](https://github.com/getbouncer/cardscan-android/tree/2.0.0087) (2021-05-13)\n\n## [2.0.0086](https://github.com/getbouncer/cardscan-android/tree/2.0.0086) (2021-05-13)\n\n**Merged pull requests:**\n\n- Remove jcenter [\\#370](https://github.com/getbouncer/cardscan-android/pull/370) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0085](https://github.com/getbouncer/cardscan-android/tree/2.0.0085) (2021-05-12)\n\n**Closed issues:**\n\n- Feature request: scan a vertical/portrait card [\\#346](https://github.com/getbouncer/cardscan-android/issues/346)\n- name and year extraction not working [\\#336](https://github.com/getbouncer/cardscan-android/issues/336)\n\n**Merged pull requests:**\n\n- Bump gradle from 4.2.0 to 4.2.1 [\\#371](https://github.com/getbouncer/cardscan-android/pull/371) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump activity-ktx from 1.2.2 to 1.2.3 [\\#369](https://github.com/getbouncer/cardscan-android/pull/369) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Bump gradle from 4.1.3 to 4.2.0 [\\#368](https://github.com/getbouncer/cardscan-android/pull/368) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Remove compile dependency on camerax [\\#367](https://github.com/getbouncer/cardscan-android/pull/367) ([awushensky](https://github.com/awushensky))\n- Support multiple cameras and add swap camera string [\\#362](https://github.com/getbouncer/cardscan-android/pull/362) ([awushensky](https://github.com/awushensky))\n- Upgrade to GitHub-native Dependabot [\\#361](https://github.com/getbouncer/cardscan-android/pull/361) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump kotlinx-serialization-json from 1.1.0 to 1.2.0 [\\#360](https://github.com/getbouncer/cardscan-android/pull/360) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump kotlin-test from 1.4.32 to 1.5.0 [\\#359](https://github.com/getbouncer/cardscan-android/pull/359) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump kotlin-gradle-plugin from 1.4.32 to 1.5.0 [\\#358](https://github.com/getbouncer/cardscan-android/pull/358) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump org.jetbrains.kotlin.plugin.serialization from 1.4.32 to 1.5.0 [\\#357](https://github.com/getbouncer/cardscan-android/pull/357) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump org.jetbrains.dokka from 1.4.30 to 1.4.32 [\\#356](https://github.com/getbouncer/cardscan-android/pull/356) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump io.github.gradle-nexus.publish-plugin from 1.0.0 to 1.1.0 [\\#354](https://github.com/getbouncer/cardscan-android/pull/354) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump fragment-ktx from 1.3.2 to 1.3.3 [\\#351](https://github.com/getbouncer/cardscan-android/pull/351) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0084](https://github.com/getbouncer/cardscan-android/tree/2.0.0084) (2021-04-20)\n\n**Closed issues:**\n\n- Fatal exception due to wrong coroutine context [\\#348](https://github.com/getbouncer/cardscan-android/issues/348)\n\n**Merged pull requests:**\n\n- Use camera2 APIs [\\#347](https://github.com/getbouncer/cardscan-android/pull/347) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0083](https://github.com/getbouncer/cardscan-android/tree/2.0.0083) (2021-04-12)\n\n**Merged pull requests:**\n\n- Ensure camera error handler runs on main thread [\\#349](https://github.com/getbouncer/cardscan-android/pull/349) ([awushensky](https://github.com/awushensky))\n- Bump kotlinx-serialization-json from 1.0.1 to 1.1.0 [\\#340](https://github.com/getbouncer/cardscan-android/pull/340) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0082](https://github.com/getbouncer/cardscan-android/tree/2.0.0082) (2021-03-30)\n\n## [2.0.0081](https://github.com/getbouncer/cardscan-android/tree/2.0.0081) (2021-03-30)\n\n**Merged pull requests:**\n\n- Correctly name the artifact for scan-payment-base [\\#345](https://github.com/getbouncer/cardscan-android/pull/345) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0080](https://github.com/getbouncer/cardscan-android/tree/2.0.0080) (2021-03-30)\n\n**Merged pull requests:**\n\n- Update kotlin versions [\\#344](https://github.com/getbouncer/cardscan-android/pull/344) ([awushensky](https://github.com/awushensky))\n- Update release names [\\#339](https://github.com/getbouncer/cardscan-android/pull/339) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0079](https://github.com/getbouncer/cardscan-android/tree/2.0.0079) (2021-03-27)\n\n**Merged pull requests:**\n\n- Swap to maven central [\\#338](https://github.com/getbouncer/cardscan-android/pull/338) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0078](https://github.com/getbouncer/cardscan-android/tree/2.0.0078) (2021-03-26)\n\n**Closed issues:**\n\n- Require specifying ML models through dependencies [\\#332](https://github.com/getbouncer/cardscan-android/issues/332)\n\n**Merged pull requests:**\n\n- Fix name and expiry extraction [\\#337](https://github.com/getbouncer/cardscan-android/pull/337) ([awushensky](https://github.com/awushensky))\n- Bump kotlin-test from 1.4.31 to 1.4.32 [\\#335](https://github.com/getbouncer/cardscan-android/pull/335) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump org.jetbrains.kotlin.plugin.serialization from 1.4.31 to 1.4.32 [\\#334](https://github.com/getbouncer/cardscan-android/pull/334) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump kotlin-gradle-plugin from 1.4.31 to 1.4.32 [\\#333](https://github.com/getbouncer/cardscan-android/pull/333) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Support minimal ML models [\\#331](https://github.com/getbouncer/cardscan-android/pull/331) ([awushensky](https://github.com/awushensky))\n- Bump gradle from 4.1.2 to 4.1.3 [\\#330](https://github.com/getbouncer/cardscan-android/pull/330) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0077](https://github.com/getbouncer/cardscan-android/tree/2.0.0077) (2021-03-23)\n\n**Merged pull requests:**\n\n- Reduce sdk size [\\#328](https://github.com/getbouncer/cardscan-android/pull/328) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0076](https://github.com/getbouncer/cardscan-android/tree/2.0.0076) (2021-03-09)\n\n## [2.0.0075](https://github.com/getbouncer/cardscan-android/tree/2.0.0075) (2021-03-06)\n\n**Merged pull requests:**\n\n- Add version code [\\#326](https://github.com/getbouncer/cardscan-android/pull/326) ([awushensky](https://github.com/awushensky))\n- Upgrade gradle and kotlin [\\#325](https://github.com/getbouncer/cardscan-android/pull/325) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0074](https://github.com/getbouncer/cardscan-android/tree/2.0.0074) (2021-02-18)\n\n## [2.0.0073](https://github.com/getbouncer/cardscan-android/tree/2.0.0073) (2021-02-18)\n\n**Closed issues:**\n\n- java.lang.NoClassDefFoundError: Failed resolution of: Lorg/tensorflow/lite/Interpreter$Options in 2.0.0072 [\\#319](https://github.com/getbouncer/cardscan-android/issues/319)\n- Card scan crash on release [\\#316](https://github.com/getbouncer/cardscan-android/issues/316)\n\n**Merged pull requests:**\n\n- Bump ktlint from 0.40.0 to 0.41.0 [\\#329](https://github.com/getbouncer/cardscan-android/pull/329) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Allow configuring no model downloads [\\#327](https://github.com/getbouncer/cardscan-android/pull/327) ([awushensky](https://github.com/awushensky))\n- Make card number display optional [\\#321](https://github.com/getbouncer/cardscan-android/pull/321) ([awushensky](https://github.com/awushensky))\n- Bump junit from 4.13.1 to 4.13.2 [\\#320](https://github.com/getbouncer/cardscan-android/pull/320) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Upgrade kotlin dependencies [\\#318](https://github.com/getbouncer/cardscan-android/pull/318) ([awushensky](https://github.com/awushensky))\n- Bump kotlin-test from 1.4.21 to 1.4.30 [\\#315](https://github.com/getbouncer/cardscan-android/pull/315) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0072](https://github.com/getbouncer/cardscan-android/tree/2.0.0072) (2021-02-04)\n\n**Closed issues:**\n\n- Crash on animation [\\#299](https://github.com/getbouncer/cardscan-android/issues/299)\n\n**Merged pull requests:**\n\n- Fix crash on fade animations when minifying resources [\\#317](https://github.com/getbouncer/cardscan-android/pull/317) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0071](https://github.com/getbouncer/cardscan-android/tree/2.0.0071) (2021-02-01)\n\n**Merged pull requests:**\n\n- Custom tensorflow lite library [\\#312](https://github.com/getbouncer/cardscan-android/pull/312) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0070](https://github.com/getbouncer/cardscan-android/tree/2.0.0070) (2021-01-29)\n\n**Merged pull requests:**\n\n- Separate ocr ux models [\\#311](https://github.com/getbouncer/cardscan-android/pull/311) ([awushensky](https://github.com/awushensky))\n- Do not expire old models [\\#310](https://github.com/getbouncer/cardscan-android/pull/310) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0069](https://github.com/getbouncer/cardscan-android/tree/2.0.0069) (2021-01-27)\n\n**Merged pull requests:**\n\n- Add cardscan local [\\#309](https://github.com/getbouncer/cardscan-android/pull/309) ([awushensky](https://github.com/awushensky))\n- Bump gradle from 4.1.1 to 4.1.2 [\\#308](https://github.com/getbouncer/cardscan-android/pull/308) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0068](https://github.com/getbouncer/cardscan-android/tree/2.0.0068) (2021-01-13)\n\n**Merged pull requests:**\n\n- Relocate payment card [\\#307](https://github.com/getbouncer/cardscan-android/pull/307) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0067](https://github.com/getbouncer/cardscan-android/tree/2.0.0067) (2021-01-11)\n\n## [2.0.0066](https://github.com/getbouncer/cardscan-android/tree/2.0.0066) (2021-01-09)\n\n## [2.0.0065](https://github.com/getbouncer/cardscan-android/tree/2.0.0065) (2021-01-09)\n\n**Merged pull requests:**\n\n- Fix memoization race condition [\\#306](https://github.com/getbouncer/cardscan-android/pull/306) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0064](https://github.com/getbouncer/cardscan-android/tree/2.0.0064) (2021-01-09)\n\n**Merged pull requests:**\n\n- Allow optional fetchers [\\#305](https://github.com/getbouncer/cardscan-android/pull/305) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0063](https://github.com/getbouncer/cardscan-android/tree/2.0.0063) (2021-01-04)\n\n## [2.0.0062](https://github.com/getbouncer/cardscan-android/tree/2.0.0062) (2020-12-31)\n\n## [2.0.0061](https://github.com/getbouncer/cardscan-android/tree/2.0.0061) (2020-12-31)\n\n**Merged pull requests:**\n\n- Improve analyzer performance [\\#301](https://github.com/getbouncer/cardscan-android/pull/301) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0060](https://github.com/getbouncer/cardscan-android/tree/2.0.0060) (2020-12-22)\n\n## [2.0.0059](https://github.com/getbouncer/cardscan-android/tree/2.0.0059) (2020-12-21)\n\n**Merged pull requests:**\n\n- Improve low-end performance [\\#300](https://github.com/getbouncer/cardscan-android/pull/300) ([awushensky](https://github.com/awushensky))\n- Update kotlin test [\\#298](https://github.com/getbouncer/cardscan-android/pull/298) ([awushensky](https://github.com/awushensky))\n- Make normalizeCardNumber public [\\#297](https://github.com/getbouncer/cardscan-android/pull/297) ([awushensky](https://github.com/awushensky))\n- Bump tensorflow-lite from 2.3.0 to 2.4.0 [\\#296](https://github.com/getbouncer/cardscan-android/pull/296) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0058](https://github.com/getbouncer/cardscan-android/tree/2.0.0058) (2020-12-15)\n\n**Merged pull requests:**\n\n- Fix clearing stats prematurely [\\#295](https://github.com/getbouncer/cardscan-android/pull/295) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0057](https://github.com/getbouncer/cardscan-android/tree/2.0.0057) (2020-12-14)\n\n**Closed issues:**\n\n- Mention new introduced strings in changelog and docs. [\\#291](https://github.com/getbouncer/cardscan-android/issues/291)\n\n**Merged pull requests:**\n\n- Use app context for network [\\#294](https://github.com/getbouncer/cardscan-android/pull/294) ([awushensky](https://github.com/awushensky))\n- Update serializer [\\#293](https://github.com/getbouncer/cardscan-android/pull/293) ([awushensky](https://github.com/awushensky))\n- Upgrade kotlin [\\#292](https://github.com/getbouncer/cardscan-android/pull/292) ([awushensky](https://github.com/awushensky))\n- Bump kotlin-test from 1.4.20 to 1.4.21 [\\#290](https://github.com/getbouncer/cardscan-android/pull/290) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump org.jetbrains.kotlin.plugin.serialization from 1.4.20 to 1.4.21 [\\#289](https://github.com/getbouncer/cardscan-android/pull/289) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump kotlin-gradle-plugin from 1.4.20 to 1.4.21 [\\#288](https://github.com/getbouncer/cardscan-android/pull/288) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump ktlint from 0.39.0 to 0.40.0 [\\#287](https://github.com/getbouncer/cardscan-android/pull/287) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0056](https://github.com/getbouncer/cardscan-android/tree/2.0.0056) (2020-12-03)\n\n**Closed issues:**\n\n- Integration of the library shows this. [\\#256](https://github.com/getbouncer/cardscan-android/issues/256)\n\n**Merged pull requests:**\n\n- Support expirys up to 100 years in the future [\\#286](https://github.com/getbouncer/cardscan-android/pull/286) ([awushensky](https://github.com/awushensky))\n- Support 3-digit CVC for amex [\\#285](https://github.com/getbouncer/cardscan-android/pull/285) ([awushensky](https://github.com/awushensky))\n- Upgrade dependencies [\\#278](https://github.com/getbouncer/cardscan-android/pull/278) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0055](https://github.com/getbouncer/cardscan-android/tree/2.0.0055) (2020-11-25)\n\n**Merged pull requests:**\n\n- Support beta model opt-in [\\#277](https://github.com/getbouncer/cardscan-android/pull/277) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0054](https://github.com/getbouncer/cardscan-android/tree/2.0.0054) (2020-11-19)\n\n**Merged pull requests:**\n\n- Use network stack for model downloads [\\#270](https://github.com/getbouncer/cardscan-android/pull/270) ([awushensky](https://github.com/awushensky))\n- Bump gradle from 4.1.0 to 4.1.1 [\\#267](https://github.com/getbouncer/cardscan-android/pull/267) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump constraintlayout from 2.0.2 to 2.0.4 [\\#262](https://github.com/getbouncer/cardscan-android/pull/262) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0053](https://github.com/getbouncer/cardscan-android/tree/2.0.0053) (2020-11-13)\n\n**Merged pull requests:**\n\n- Make permissions requests make more sense [\\#269](https://github.com/getbouncer/cardscan-android/pull/269) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0052](https://github.com/getbouncer/cardscan-android/tree/2.0.0052) (2020-11-13)\n\n**Merged pull requests:**\n\n- Reset stats at scan start [\\#268](https://github.com/getbouncer/cardscan-android/pull/268) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0051](https://github.com/getbouncer/cardscan-android/tree/2.0.0051) (2020-11-10)\n\n**Merged pull requests:**\n\n- Do not use Date.toString due to crashes [\\#266](https://github.com/getbouncer/cardscan-android/pull/266) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0050](https://github.com/getbouncer/cardscan-android/tree/2.0.0050) (2020-10-26)\n\n**Merged pull requests:**\n\n- Upgrade gradle to 4.1.0 [\\#257](https://github.com/getbouncer/cardscan-android/pull/257) ([awushensky](https://github.com/awushensky))\n- Send model hash with info request [\\#250](https://github.com/getbouncer/cardscan-android/pull/250) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0049](https://github.com/getbouncer/cardscan-android/tree/2.0.0049) (2020-10-21)\n\n**Merged pull requests:**\n\n- Name and expiry in completion loop [\\#255](https://github.com/getbouncer/cardscan-android/pull/255) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0048](https://github.com/getbouncer/cardscan-android/tree/2.0.0048) (2020-10-19)\n\n**Merged pull requests:**\n\n- Use latest version in loader [\\#254](https://github.com/getbouncer/cardscan-android/pull/254) ([awushensky](https://github.com/awushensky))\n- Fix stats concurrent modification [\\#253](https://github.com/getbouncer/cardscan-android/pull/253) ([awushensky](https://github.com/awushensky))\n- Enable delay analysis [\\#252](https://github.com/getbouncer/cardscan-android/pull/252) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0047](https://github.com/getbouncer/cardscan-android/tree/2.0.0047) (2020-10-15)\n\n**Merged pull requests:**\n\n- Remove autofocus feature requirement [\\#251](https://github.com/getbouncer/cardscan-android/pull/251) ([awushensky](https://github.com/awushensky))\n\n## [1.0.5155](https://github.com/getbouncer/cardscan-android/tree/1.0.5155) (2020-10-15)\n\n## [2.0.0046](https://github.com/getbouncer/cardscan-android/tree/2.0.0046) (2020-10-14)\n\n## [2.0.0045](https://github.com/getbouncer/cardscan-android/tree/2.0.0045) (2020-10-12)\n\n**Merged pull requests:**\n\n- Upgrade gradle [\\#248](https://github.com/getbouncer/cardscan-android/pull/248) ([awushensky](https://github.com/awushensky))\n- Force model download if no cache [\\#247](https://github.com/getbouncer/cardscan-android/pull/247) ([awushensky](https://github.com/awushensky))\n- Bump junit from 4.13 to 4.13.1 [\\#246](https://github.com/getbouncer/cardscan-android/pull/246) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump constraintlayout from 2.0.1 to 2.0.2 [\\#241](https://github.com/getbouncer/cardscan-android/pull/241) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [1.0.5153](https://github.com/getbouncer/cardscan-android/tree/1.0.5153) (2020-10-09)\n\n## [1.0.5154](https://github.com/getbouncer/cardscan-android/tree/1.0.5154) (2020-10-09)\n\n**Merged pull requests:**\n\n- Fix zoom image [\\#245](https://github.com/getbouncer/cardscan-android/pull/245) ([awushensky](https://github.com/awushensky))\n- Support zoomed model [\\#236](https://github.com/getbouncer/cardscan-android/pull/236) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0044](https://github.com/getbouncer/cardscan-android/tree/2.0.0044) (2020-10-09)\n\n**Merged pull requests:**\n\n- Parallelize model downloads [\\#244](https://github.com/getbouncer/cardscan-android/pull/244) ([awushensky](https://github.com/awushensky))\n- Clean up some OCR [\\#243](https://github.com/getbouncer/cardscan-android/pull/243) ([awushensky](https://github.com/awushensky))\n- Improve fetcher logging [\\#242](https://github.com/getbouncer/cardscan-android/pull/242) ([awushensky](https://github.com/awushensky))\n- Target android 30 [\\#240](https://github.com/getbouncer/cardscan-android/pull/240) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0043](https://github.com/getbouncer/cardscan-android/tree/2.0.0043) (2020-10-06)\n\n**Merged pull requests:**\n\n- Fix autofocus more [\\#239](https://github.com/getbouncer/cardscan-android/pull/239) ([awushensky](https://github.com/awushensky))\n- Remove camera1 autofocus repeater [\\#238](https://github.com/getbouncer/cardscan-android/pull/238) ([awushensky](https://github.com/awushensky))\n- Clean up image utils [\\#237](https://github.com/getbouncer/cardscan-android/pull/237) ([awushensky](https://github.com/awushensky))\n\n## [1.0.5152](https://github.com/getbouncer/cardscan-android/tree/1.0.5152) (2020-10-05)\n\n**Merged pull requests:**\n\n- Upgrade ocr model [\\#235](https://github.com/getbouncer/cardscan-android/pull/235) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0042](https://github.com/getbouncer/cardscan-android/tree/2.0.0042) (2020-10-02)\n\n**Merged pull requests:**\n\n- Support model upgrade delay [\\#234](https://github.com/getbouncer/cardscan-android/pull/234) ([awushensky](https://github.com/awushensky))\n- Bump core-ktx from 1.3.1 to 1.3.2 [\\#233](https://github.com/getbouncer/cardscan-android/pull/233) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0041](https://github.com/getbouncer/cardscan-android/tree/2.0.0041) (2020-10-02)\n\n## [2.0.0040](https://github.com/getbouncer/cardscan-android/tree/2.0.0040) (2020-10-01)\n\n**Merged pull requests:**\n\n- Fix cancelation memory leak [\\#232](https://github.com/getbouncer/cardscan-android/pull/232) ([awushensky](https://github.com/awushensky))\n- Fix frame saver memory leak [\\#231](https://github.com/getbouncer/cardscan-android/pull/231) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0039](https://github.com/getbouncer/cardscan-android/tree/2.0.0039) (2020-09-30)\n\n**Merged pull requests:**\n\n- Tune the name & expiry extraction analyzers [\\#230](https://github.com/getbouncer/cardscan-android/pull/230) ([awushensky](https://github.com/awushensky))\n- Split main loop [\\#229](https://github.com/getbouncer/cardscan-android/pull/229) ([awushensky](https://github.com/awushensky))\n- Calculate and adapt to device speed [\\#228](https://github.com/getbouncer/cardscan-android/pull/228) ([awushensky](https://github.com/awushensky))\n- Prevent duplicate final results [\\#227](https://github.com/getbouncer/cardscan-android/pull/227) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0038](https://github.com/getbouncer/cardscan-android/tree/2.0.0038) (2020-09-25)\n\n**Merged pull requests:**\n\n- Clean up resources [\\#226](https://github.com/getbouncer/cardscan-android/pull/226) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0037](https://github.com/getbouncer/cardscan-android/tree/2.0.0037) (2020-09-25)\n\n**Closed issues:**\n\n- Demo app crashed [\\#223](https://github.com/getbouncer/cardscan-android/issues/223)\n\n## [2.0.0036](https://github.com/getbouncer/cardscan-android/tree/2.0.0036) (2020-09-24)\n\n**Merged pull requests:**\n\n- Clean up single activity demo [\\#225](https://github.com/getbouncer/cardscan-android/pull/225) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0035](https://github.com/getbouncer/cardscan-android/tree/2.0.0035) (2020-09-24)\n\n**Merged pull requests:**\n\n- Fix crash on single activity demo [\\#224](https://github.com/getbouncer/cardscan-android/pull/224) ([awushensky](https://github.com/awushensky))\n- Clean up UI changes [\\#222](https://github.com/getbouncer/cardscan-android/pull/222) ([awushensky](https://github.com/awushensky))\n- Add java continuation support [\\#221](https://github.com/getbouncer/cardscan-android/pull/221) ([awushensky](https://github.com/awushensky))\n- Create programmatic UI [\\#217](https://github.com/getbouncer/cardscan-android/pull/217) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0034](https://github.com/getbouncer/cardscan-android/tree/2.0.0034) (2020-09-22)\n\n**Merged pull requests:**\n\n- Allow manual camera pause [\\#216](https://github.com/getbouncer/cardscan-android/pull/216) ([awushensky](https://github.com/awushensky))\n- Bump kotlin-stdlib-jdk7 from 1.4.0 to 1.4.10 [\\#215](https://github.com/getbouncer/cardscan-android/pull/215) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump kotlin-test from 1.4.0 to 1.4.10 [\\#214](https://github.com/getbouncer/cardscan-android/pull/214) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump ktlint from 0.38.1 to 0.39.0 [\\#213](https://github.com/getbouncer/cardscan-android/pull/213) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0033](https://github.com/getbouncer/cardscan-android/tree/2.0.0033) (2020-09-17)\n\n## [2.0.0032](https://github.com/getbouncer/cardscan-android/tree/2.0.0032) (2020-09-11)\n\n**Merged pull requests:**\n\n- Fix a camera crash on revvl2 devices [\\#212](https://github.com/getbouncer/cardscan-android/pull/212) ([awushensky](https://github.com/awushensky))\n- Support better camera autofocus [\\#211](https://github.com/getbouncer/cardscan-android/pull/211) ([awushensky](https://github.com/awushensky))\n- Update state machine tests [\\#210](https://github.com/getbouncer/cardscan-android/pull/210) ([awushensky](https://github.com/awushensky))\n- Bump kotlin-gradle-plugin from 1.4.0 to 1.4.10 [\\#209](https://github.com/getbouncer/cardscan-android/pull/209) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump org.jetbrains.kotlin.plugin.serialization from 1.4.0 to 1.4.10 [\\#208](https://github.com/getbouncer/cardscan-android/pull/208) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0031](https://github.com/getbouncer/cardscan-android/tree/2.0.0031) (2020-09-11)\n\n**Merged pull requests:**\n\n- fixes the bitmap test [\\#207](https://github.com/getbouncer/cardscan-android/pull/207) ([dxaen](https://github.com/dxaen))\n\n## [2.0.0030](https://github.com/getbouncer/cardscan-android/tree/2.0.0030) (2020-09-08)\n\n**Closed issues:**\n\n- Analyzer failure with DexGuard enabled [\\#202](https://github.com/getbouncer/cardscan-android/issues/202)\n\n**Merged pull requests:**\n\n- Add unit tests for cardscan state machine [\\#206](https://github.com/getbouncer/cardscan-android/pull/206) ([awushensky](https://github.com/awushensky))\n- Quick read support for android [\\#203](https://github.com/getbouncer/cardscan-android/pull/203) ([dxaen](https://github.com/dxaen))\n\n## [2.0.0029](https://github.com/getbouncer/cardscan-android/tree/2.0.0029) (2020-09-08)\n\n**Merged pull requests:**\n\n- Add proguard rules for tensorflow [\\#205](https://github.com/getbouncer/cardscan-android/pull/205) ([awushensky](https://github.com/awushensky))\n- Standardize expiry to strings [\\#204](https://github.com/getbouncer/cardscan-android/pull/204) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0028](https://github.com/getbouncer/cardscan-android/tree/2.0.0028) (2020-09-03)\n\n**Merged pull requests:**\n\n- Open up the UI [\\#201](https://github.com/getbouncer/cardscan-android/pull/201) ([awushensky](https://github.com/awushensky))\n- Add card payment type data [\\#198](https://github.com/getbouncer/cardscan-android/pull/198) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0027](https://github.com/getbouncer/cardscan-android/tree/2.0.0027) (2020-09-01)\n\n**Merged pull requests:**\n\n- Clean up state machine [\\#199](https://github.com/getbouncer/cardscan-android/pull/199) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0026](https://github.com/getbouncer/cardscan-android/tree/2.0.0026) (2020-08-27)\n\n**Merged pull requests:**\n\n- Bump junit from 1.1.1 to 1.1.2 [\\#197](https://github.com/getbouncer/cardscan-android/pull/197) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump espresso-core from 3.2.0 to 3.3.0 [\\#196](https://github.com/getbouncer/cardscan-android/pull/196) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump runner from 1.2.0 to 1.3.0 [\\#195](https://github.com/getbouncer/cardscan-android/pull/195) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump core from 1.2.0 to 1.3.0 [\\#194](https://github.com/getbouncer/cardscan-android/pull/194) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump constraintlayout from 2.0.0 to 2.0.1 [\\#193](https://github.com/getbouncer/cardscan-android/pull/193) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0025](https://github.com/getbouncer/cardscan-android/tree/2.0.0025) (2020-08-24)\n\n**Merged pull requests:**\n\n- Bump constraintlayout from 1.1.3 to 2.0.0 [\\#192](https://github.com/getbouncer/cardscan-android/pull/192) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0024](https://github.com/getbouncer/cardscan-android/tree/2.0.0024) (2020-08-21)\n\n## [2.0.0023](https://github.com/getbouncer/cardscan-android/tree/2.0.0023) (2020-08-21)\n\n**Merged pull requests:**\n\n- Enable minification on cardscan demo [\\#191](https://github.com/getbouncer/cardscan-android/pull/191) ([awushensky](https://github.com/awushensky))\n- Relocate ktlint [\\#190](https://github.com/getbouncer/cardscan-android/pull/190) ([awushensky](https://github.com/awushensky))\n- Fix accessibility descriptions [\\#189](https://github.com/getbouncer/cardscan-android/pull/189) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0022](https://github.com/getbouncer/cardscan-android/tree/2.0.0022) (2020-08-18)\n\n## [2.0.0021](https://github.com/getbouncer/cardscan-android/tree/2.0.0021) (2020-08-18)\n\n**Closed issues:**\n\n- How to scan other types of cards? [\\#150](https://github.com/getbouncer/cardscan-android/issues/150)\n\n**Merged pull requests:**\n\n- Chang custom card issuer [\\#185](https://github.com/getbouncer/cardscan-android/pull/185) ([smkuhne](https://github.com/smkuhne))\n- Bump kotlinx-serialization-runtime from 1.0-M1-1.4.0-rc to 1.0-M1-1.4.0-rc-218 [\\#184](https://github.com/getbouncer/cardscan-android/pull/184) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add custom pans [\\#183](https://github.com/getbouncer/cardscan-android/pull/183) ([smkuhne](https://github.com/smkuhne))\n- Update dependencies [\\#182](https://github.com/getbouncer/cardscan-android/pull/182) ([awushensky](https://github.com/awushensky))\n- Local rules [\\#181](https://github.com/getbouncer/cardscan-android/pull/181) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0020](https://github.com/getbouncer/cardscan-android/tree/2.0.0020) (2020-08-13)\n\n**Closed issues:**\n\n- Crash on android 5 Lenovo [\\#89](https://github.com/getbouncer/cardscan-android/issues/89)\n\n**Merged pull requests:**\n\n- Add aspect ratio method [\\#173](https://github.com/getbouncer/cardscan-android/pull/173) ([smkuhne](https://github.com/smkuhne))\n- Prevent crash on bad model download [\\#172](https://github.com/getbouncer/cardscan-android/pull/172) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0019](https://github.com/getbouncer/cardscan-android/tree/2.0.0019) (2020-08-12)\n\n**Merged pull requests:**\n\n- Fix display bug [\\#171](https://github.com/getbouncer/cardscan-android/pull/171) ([awushensky](https://github.com/awushensky))\n- Support extracting iin and last4 from utils [\\#170](https://github.com/getbouncer/cardscan-android/pull/170) ([awushensky](https://github.com/awushensky))\n- Add check result [\\#169](https://github.com/getbouncer/cardscan-android/pull/169) ([awushensky](https://github.com/awushensky))\n- Bump appcompat from 1.1.0 to 1.2.0 [\\#168](https://github.com/getbouncer/cardscan-android/pull/168) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add zoomOffset method [\\#167](https://github.com/getbouncer/cardscan-android/pull/167) ([smkuhne](https://github.com/smkuhne))\n- Use key without permissions for test [\\#166](https://github.com/getbouncer/cardscan-android/pull/166) ([awushensky](https://github.com/awushensky))\n- Update expiry timeout, handle new permissions [\\#165](https://github.com/getbouncer/cardscan-android/pull/165) ([xsl](https://github.com/xsl))\n- Add documentation [\\#164](https://github.com/getbouncer/cardscan-android/pull/164) ([awushensky](https://github.com/awushensky))\n- Bump tensorflow-lite from 2.2.0 to 2.3.0 [\\#163](https://github.com/getbouncer/cardscan-android/pull/163) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump core-ktx from 1.3.0 to 1.3.1 [\\#159](https://github.com/getbouncer/cardscan-android/pull/159) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0018](https://github.com/getbouncer/cardscan-android/tree/2.0.0018) (2020-07-28)\n\n**Closed issues:**\n\n- name and expiry date [\\#82](https://github.com/getbouncer/cardscan-android/issues/82)\n\n**Merged pull requests:**\n\n- Clean up counters [\\#162](https://github.com/getbouncer/cardscan-android/pull/162) ([awushensky](https://github.com/awushensky))\n- Clean up state machine logic [\\#161](https://github.com/getbouncer/cardscan-android/pull/161) ([awushensky](https://github.com/awushensky))\n- Clean up state machine [\\#160](https://github.com/getbouncer/cardscan-android/pull/160) ([awushensky](https://github.com/awushensky))\n- Main loop state machine [\\#158](https://github.com/getbouncer/cardscan-android/pull/158) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0017](https://github.com/getbouncer/cardscan-android/tree/2.0.0017) (2020-07-22)\n\n**Merged pull requests:**\n\n- Update api key validation check [\\#156](https://github.com/getbouncer/cardscan-android/pull/156) ([awushensky](https://github.com/awushensky))\n- Update changelog [\\#155](https://github.com/getbouncer/cardscan-android/pull/155) ([smkuhne](https://github.com/smkuhne))\n\n## [2.0.0016](https://github.com/getbouncer/cardscan-android/tree/2.0.0016) (2020-07-20)\n\n**Merged pull requests:**\n\n- Bump version to 2.0.0016 [\\#154](https://github.com/getbouncer/cardscan-android/pull/154) ([smkuhne](https://github.com/smkuhne))\n- Add readmes [\\#153](https://github.com/getbouncer/cardscan-android/pull/153) ([awushensky](https://github.com/awushensky))\n- Rename demo module [\\#152](https://github.com/getbouncer/cardscan-android/pull/152) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0015](https://github.com/getbouncer/cardscan-android/tree/2.0.0015) (2020-07-18)\n\n**Merged pull requests:**\n\n- Restructure 2.0 [\\#151](https://github.com/getbouncer/cardscan-android/pull/151) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0014](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0014) (2020-07-14)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0013...2.0.0014)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0013...2.0.0014)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0013...2.0.0014)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0013...2.0.0014)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0013...2.0.0014)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0013...2.0.0014)\n\n**Merged pull requests:**\n\n- Image fragmentation [\\#53](https://github.com/getbouncer/scan-framework-android/pull/53) ([smkuhne](https://github.com/smkuhne))\n- Standardize models [\\#35](https://github.com/getbouncer/scan-payment-android/pull/35) ([awushensky](https://github.com/awushensky))\n- Image fragmentation [\\#33](https://github.com/getbouncer/scan-payment-android/pull/33) ([smkuhne](https://github.com/smkuhne))\n- Allow extending uploadstats [\\#30](https://github.com/getbouncer/scan-ui-android/pull/30) ([awushensky](https://github.com/awushensky))\n- Image fragmentation [\\#28](https://github.com/getbouncer/scan-ui-android/pull/28) ([smkuhne](https://github.com/smkuhne))\n\n## [2.0.0013](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0013) (2020-07-08)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0012...2.0.0013)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0012...2.0.0013)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0012...2.0.0013)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0012...2.0.0013)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0012...2.0.0013)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0012...2.0.0013)\n\n**Merged pull requests:**\n\n- Bump ktlint from 0.37.0 to 0.37.2 [\\#59](https://github.com/getbouncer/cardscan-demo-android/pull/59) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Use new warmup initializer [\\#58](https://github.com/getbouncer/cardscan-demo-android/pull/58) ([xsl](https://github.com/xsl))\n- Update UI [\\#57](https://github.com/getbouncer/cardscan-demo-android/pull/57) ([awushensky](https://github.com/awushensky))\n- Update for expiry extraction + new text detector [\\#56](https://github.com/getbouncer/cardscan-demo-android/pull/56) ([xsl](https://github.com/xsl))\n- Add more java interoperability [\\#57](https://github.com/getbouncer/scan-framework-android/pull/57) ([awushensky](https://github.com/awushensky))\n- Relocate loop state to result [\\#56](https://github.com/getbouncer/scan-framework-android/pull/56) ([awushensky](https://github.com/awushensky))\n- Add more memoize functions [\\#55](https://github.com/getbouncer/scan-framework-android/pull/55) ([awushensky](https://github.com/awushensky))\n- Separate error listeners [\\#54](https://github.com/getbouncer/scan-framework-android/pull/54) ([awushensky](https://github.com/awushensky))\n- Fix work leak [\\#52](https://github.com/getbouncer/scan-framework-android/pull/52) ([awushensky](https://github.com/awushensky))\n- Test duration [\\#51](https://github.com/getbouncer/scan-framework-android/pull/51) ([awushensky](https://github.com/awushensky))\n- Clean up network responses [\\#49](https://github.com/getbouncer/scan-framework-android/pull/49) ([awushensky](https://github.com/awushensky))\n- Add exceptions to retry [\\#48](https://github.com/getbouncer/scan-framework-android/pull/48) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.37.0 to 0.37.2 [\\#46](https://github.com/getbouncer/scan-framework-android/pull/46) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add device identifier to network requests [\\#45](https://github.com/getbouncer/scan-framework-android/pull/45) ([awushensky](https://github.com/awushensky))\n- Standardize local file name [\\#44](https://github.com/getbouncer/scan-framework-android/pull/44) ([awushensky](https://github.com/awushensky))\n- Add some convenience functions [\\#43](https://github.com/getbouncer/scan-framework-android/pull/43) ([xsl](https://github.com/xsl))\n- Launch the camera on IO dispatcher [\\#32](https://github.com/getbouncer/scan-camera-android/pull/32) ([awushensky](https://github.com/awushensky))\n- Relocate analyzers [\\#34](https://github.com/getbouncer/scan-payment-android/pull/34) ([awushensky](https://github.com/awushensky))\n- Support multiple MM/YYs on cards [\\#32](https://github.com/getbouncer/scan-payment-android/pull/32) ([xsl](https://github.com/xsl))\n- Bump ktlint from 0.37.0 to 0.37.2 [\\#31](https://github.com/getbouncer/scan-payment-android/pull/31) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Expiry extraction + new text detector [\\#30](https://github.com/getbouncer/scan-payment-android/pull/30) ([xsl](https://github.com/xsl))\n- Update submodule [\\#29](https://github.com/getbouncer/scan-ui-android/pull/29) ([awushensky](https://github.com/awushensky))\n- Separate scan stats [\\#27](https://github.com/getbouncer/scan-ui-android/pull/27) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.37.0 to 0.37.2 [\\#26](https://github.com/getbouncer/scan-ui-android/pull/26) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add initialization error strings [\\#25](https://github.com/getbouncer/scan-ui-android/pull/25) ([xsl](https://github.com/xsl))\n- Update UI [\\#24](https://github.com/getbouncer/scan-ui-android/pull/24) ([awushensky](https://github.com/awushensky))\n- Update scan-framework-android submodule [\\#23](https://github.com/getbouncer/scan-ui-android/pull/23) ([xsl](https://github.com/xsl))\n- Shut down analyzer context on quit [\\#38](https://github.com/getbouncer/cardscan-ui-android/pull/38) ([awushensky](https://github.com/awushensky))\n- Separate loop logic [\\#37](https://github.com/getbouncer/cardscan-ui-android/pull/37) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.37.0 to 0.37.2 [\\#35](https://github.com/getbouncer/cardscan-ui-android/pull/35) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update ui [\\#34](https://github.com/getbouncer/cardscan-ui-android/pull/34) ([awushensky](https://github.com/awushensky))\n- Warmup name and expiry [\\#33](https://github.com/getbouncer/cardscan-ui-android/pull/33) ([xsl](https://github.com/xsl))\n- Expiry extraction + new text detector [\\#32](https://github.com/getbouncer/cardscan-ui-android/pull/32) ([xsl](https://github.com/xsl))\n\n## [2.0.0012](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0012) (2020-06-15)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0011...2.0.0012)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0011...2.0.0012)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0011...2.0.0012)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0011...2.0.0012)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0011...2.0.0012)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0011...2.0.0012)\n\n**Merged pull requests:**\n\n- Rename warm up [\\#55](https://github.com/getbouncer/cardscan-demo-android/pull/55) ([awushensky](https://github.com/awushensky))\n- Use flows instead of channels [\\#42](https://github.com/getbouncer/scan-framework-android/pull/42) ([awushensky](https://github.com/awushensky))\n- Use channels better [\\#41](https://github.com/getbouncer/scan-framework-android/pull/41) ([awushensky](https://github.com/awushensky))\n- Fix framerate average calculation [\\#40](https://github.com/getbouncer/scan-framework-android/pull/40) ([awushensky](https://github.com/awushensky))\n- Make result handlers listen to a lifecycle [\\#39](https://github.com/getbouncer/scan-framework-android/pull/39) ([awushensky](https://github.com/awushensky))\n- Move images out of framework [\\#38](https://github.com/getbouncer/scan-framework-android/pull/38) ([awushensky](https://github.com/awushensky))\n- Support java interop [\\#37](https://github.com/getbouncer/scan-framework-android/pull/37) ([awushensky](https://github.com/awushensky))\n- Fix camera crash on flash not supported [\\#30](https://github.com/getbouncer/scan-camera-android/pull/30) ([awushensky](https://github.com/awushensky))\n- Increase the channel buffer size to 2 [\\#29](https://github.com/getbouncer/scan-camera-android/pull/29) ([awushensky](https://github.com/awushensky))\n- Reintroduce camera2 [\\#28](https://github.com/getbouncer/scan-camera-android/pull/28) ([awushensky](https://github.com/awushensky))\n- Centralize the channel logic [\\#27](https://github.com/getbouncer/scan-camera-android/pull/27) ([awushensky](https://github.com/awushensky))\n- Remove framework submodule [\\#26](https://github.com/getbouncer/scan-camera-android/pull/26) ([awushensky](https://github.com/awushensky))\n- Add timing to ssdocr input [\\#29](https://github.com/getbouncer/scan-payment-android/pull/29) ([awushensky](https://github.com/awushensky))\n- Relocate test resources [\\#28](https://github.com/getbouncer/scan-payment-android/pull/28) ([awushensky](https://github.com/awushensky))\n- Relocate image manipulation utilities [\\#27](https://github.com/getbouncer/scan-payment-android/pull/27) ([awushensky](https://github.com/awushensky))\n- Use flows [\\#22](https://github.com/getbouncer/scan-ui-android/pull/22) ([awushensky](https://github.com/awushensky))\n- Relocate scan process [\\#21](https://github.com/getbouncer/scan-ui-android/pull/21) ([awushensky](https://github.com/awushensky))\n- Separate framework from camera [\\#20](https://github.com/getbouncer/scan-ui-android/pull/20) ([awushensky](https://github.com/awushensky))\n- Handle devices without cameras [\\#19](https://github.com/getbouncer/scan-ui-android/pull/19) ([awushensky](https://github.com/awushensky))\n- Reduce jitter in name display [\\#31](https://github.com/getbouncer/cardscan-ui-android/pull/31) ([awushensky](https://github.com/awushensky))\n- Fix analyzer pool memory leak [\\#30](https://github.com/getbouncer/cardscan-ui-android/pull/30) ([awushensky](https://github.com/awushensky))\n- Actually reset the result aggregator [\\#29](https://github.com/getbouncer/cardscan-ui-android/pull/29) ([awushensky](https://github.com/awushensky))\n- Relocate scan logic [\\#28](https://github.com/getbouncer/cardscan-ui-android/pull/28) ([awushensky](https://github.com/awushensky))\n- Relocate scan flow to implementation [\\#27](https://github.com/getbouncer/cardscan-ui-android/pull/27) ([awushensky](https://github.com/awushensky))\n- Separate camera and loops [\\#26](https://github.com/getbouncer/cardscan-ui-android/pull/26) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0011](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0011) (2020-06-08)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0009...2.0.0011)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0010...2.0.0011)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0010...2.0.0011)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0010...2.0.0011)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0010...2.0.0011)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0009...2.0.0011)\n\n**Merged pull requests:**\n\n- Support separate name extraction parameters [\\#54](https://github.com/getbouncer/cardscan-demo-android/pull/54) ([awushensky](https://github.com/awushensky))\n- Add docs and buttons for name extraction [\\#53](https://github.com/getbouncer/cardscan-demo-android/pull/53) ([xsl](https://github.com/xsl))\n- Reduce name extraction settings [\\#52](https://github.com/getbouncer/cardscan-demo-android/pull/52) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.36.0 to 0.37.0 [\\#51](https://github.com/getbouncer/cardscan-demo-android/pull/51) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update for name extraction [\\#50](https://github.com/getbouncer/cardscan-demo-android/pull/50) ([xsl](https://github.com/xsl))\n- Bump gradle from 3.6.3 to 4.0.0 [\\#47](https://github.com/getbouncer/cardscan-demo-android/pull/47) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update coroutine interoperability [\\#36](https://github.com/getbouncer/scan-framework-android/pull/36) ([awushensky](https://github.com/awushensky))\n- Standardize result counter [\\#35](https://github.com/getbouncer/scan-framework-android/pull/35) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.36.0 to 0.37.0 [\\#34](https://github.com/getbouncer/scan-framework-android/pull/34) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add memoization functions [\\#33](https://github.com/getbouncer/scan-framework-android/pull/33) ([awushensky](https://github.com/awushensky))\n- Don't retry on FileNotFoundExceptions [\\#32](https://github.com/getbouncer/scan-framework-android/pull/32) ([xsl](https://github.com/xsl))\n- Update image utilities [\\#31](https://github.com/getbouncer/scan-framework-android/pull/31) ([awushensky](https://github.com/awushensky))\n- Use default dispatcher for camera [\\#25](https://github.com/getbouncer/scan-camera-android/pull/25) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.36.0 to 0.37.0 [\\#24](https://github.com/getbouncer/scan-camera-android/pull/24) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update submodule [\\#23](https://github.com/getbouncer/scan-camera-android/pull/23) ([xsl](https://github.com/xsl))\n- Update image utils [\\#22](https://github.com/getbouncer/scan-camera-android/pull/22) ([awushensky](https://github.com/awushensky))\n- Remove threadsafe flag [\\#26](https://github.com/getbouncer/scan-payment-android/pull/26) ([awushensky](https://github.com/awushensky))\n- Minor tuning for name extraction [\\#25](https://github.com/getbouncer/scan-payment-android/pull/25) ([xsl](https://github.com/xsl))\n- Relocate object detect test [\\#24](https://github.com/getbouncer/scan-payment-android/pull/24) ([awushensky](https://github.com/awushensky))\n- Remove debug log [\\#23](https://github.com/getbouncer/scan-payment-android/pull/23) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.36.0 to 0.37.0 [\\#22](https://github.com/getbouncer/scan-payment-android/pull/22) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update image utils [\\#21](https://github.com/getbouncer/scan-payment-android/pull/21) ([awushensky](https://github.com/awushensky))\n- Add NameDetectAnalyzer and move object detector over \\(for now at least\\) [\\#20](https://github.com/getbouncer/scan-payment-android/pull/20) ([xsl](https://github.com/xsl))\n- Remove unnecessary manifest entries [\\#18](https://github.com/getbouncer/scan-ui-android/pull/18) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.36.0 to 0.37.0 [\\#17](https://github.com/getbouncer/scan-ui-android/pull/17) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Introduce fadeIn duration config and update card border animation duration [\\#16](https://github.com/getbouncer/scan-ui-android/pull/16) ([xsl](https://github.com/xsl))\n- Support java interoperability [\\#25](https://github.com/getbouncer/cardscan-ui-android/pull/25) ([awushensky](https://github.com/awushensky))\n- Enable disabling name extraction on start [\\#24](https://github.com/getbouncer/cardscan-ui-android/pull/24) ([awushensky](https://github.com/awushensky))\n- Update scan payments submodule [\\#23](https://github.com/getbouncer/cardscan-ui-android/pull/23) ([xsl](https://github.com/xsl))\n- Use default dispatchers [\\#22](https://github.com/getbouncer/cardscan-ui-android/pull/22) ([awushensky](https://github.com/awushensky))\n- Reduce settings for name extractor [\\#21](https://github.com/getbouncer/cardscan-ui-android/pull/21) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.36.0 to 0.37.0 [\\#20](https://github.com/getbouncer/cardscan-ui-android/pull/20) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update image utils [\\#19](https://github.com/getbouncer/cardscan-ui-android/pull/19) ([awushensky](https://github.com/awushensky))\n- Name extraction v1 w/ old object detector [\\#18](https://github.com/getbouncer/cardscan-ui-android/pull/18) ([xsl](https://github.com/xsl))\n- Bump gradle from 3.6.3 to 4.0.0 [\\#17](https://github.com/getbouncer/cardscan-ui-android/pull/17) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0010](https://github.com/getbouncer/scan-framework-android/tree/2.0.0010) (2020-05-30)\n\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0009...2.0.0010)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0009...2.0.0010)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0009...2.0.0010)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0009...2.0.0010)\n\n**Merged pull requests:**\n\n- Terminate a finite loop that has no data [\\#30](https://github.com/getbouncer/scan-framework-android/pull/30) ([awushensky](https://github.com/awushensky))\n- Bump gradle from 3.6.3 to 4.0.0 [\\#29](https://github.com/getbouncer/scan-framework-android/pull/29) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Better support camera flash [\\#21](https://github.com/getbouncer/scan-camera-android/pull/21) ([awushensky](https://github.com/awushensky))\n- Bump gradle from 3.6.3 to 4.0.0 [\\#20](https://github.com/getbouncer/scan-camera-android/pull/20) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump gradle from 3.6.3 to 4.0.0 [\\#19](https://github.com/getbouncer/scan-payment-android/pull/19) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump gradle from 3.6.3 to 4.0.0 [\\#15](https://github.com/getbouncer/scan-ui-android/pull/15) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0009](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0009) (2020-05-29)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0008...2.0.0009)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0008...2.0.0009)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0008...2.0.0009)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0008...2.0.0009)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0008...2.0.0009)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0008...2.0.0009)\n\n**Merged pull requests:**\n\n- Bump core-ktx from 1.2.0 to 1.3.0 [\\#46](https://github.com/getbouncer/cardscan-demo-android/pull/46) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Remove required card number [\\#45](https://github.com/getbouncer/cardscan-demo-android/pull/45) ([awushensky](https://github.com/awushensky))\n- Bump core-ktx from 1.2.0 to 1.3.0 [\\#28](https://github.com/getbouncer/scan-framework-android/pull/28) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Start aggregation timer on valid result [\\#27](https://github.com/getbouncer/scan-framework-android/pull/27) ([awushensky](https://github.com/awushensky))\n- Stop ignoring scan timeout [\\#26](https://github.com/getbouncer/scan-framework-android/pull/26) ([awushensky](https://github.com/awushensky))\n- Simplify results [\\#25](https://github.com/getbouncer/scan-framework-android/pull/25) ([awushensky](https://github.com/awushensky))\n- Add logging to stats [\\#24](https://github.com/getbouncer/scan-framework-android/pull/24) ([awushensky](https://github.com/awushensky))\n- Bump core-ktx from 1.2.0 to 1.3.0 [\\#19](https://github.com/getbouncer/scan-camera-android/pull/19) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Use random focus variance [\\#18](https://github.com/getbouncer/scan-camera-android/pull/18) ([awushensky](https://github.com/awushensky))\n- Bump core-ktx from 1.2.0 to 1.3.0 [\\#18](https://github.com/getbouncer/scan-payment-android/pull/18) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Relocate aggregator [\\#17](https://github.com/getbouncer/scan-payment-android/pull/17) ([awushensky](https://github.com/awushensky))\n- Bump core-ktx from 1.2.0 to 1.3.0 [\\#14](https://github.com/getbouncer/scan-ui-android/pull/14) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Keep the screen on while scanning [\\#13](https://github.com/getbouncer/scan-ui-android/pull/13) ([awushensky](https://github.com/awushensky))\n- Reset previously valid result [\\#16](https://github.com/getbouncer/cardscan-ui-android/pull/16) ([awushensky](https://github.com/awushensky))\n- Bump core-ktx from 1.2.0 to 1.3.0 [\\#15](https://github.com/getbouncer/cardscan-ui-android/pull/15) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Remove isValidPan [\\#14](https://github.com/getbouncer/cardscan-ui-android/pull/14) ([awushensky](https://github.com/awushensky))\n- Start result aggregation on valid result [\\#13](https://github.com/getbouncer/cardscan-ui-android/pull/13) ([awushensky](https://github.com/awushensky))\n- Simplify results [\\#12](https://github.com/getbouncer/cardscan-ui-android/pull/12) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0008](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0008) (2020-05-21)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0007...2.0.0008)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0007...2.0.0008)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0007...2.0.0008)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0007...2.0.0008)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0007...2.0.0008)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0007...2.0.0008)\n\n**Merged pull requests:**\n\n- Bump kotlinx-coroutines-core from 1.3.6 to 1.3.7 [\\#43](https://github.com/getbouncer/cardscan-demo-android/pull/43) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.jfrog.bintray from 1.7.3 to 1.8.5 [\\#42](https://github.com/getbouncer/cardscan-demo-android/pull/42) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.github.dcendents.android-maven from 2.0 to 2.1 [\\#41](https://github.com/getbouncer/cardscan-demo-android/pull/41) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Make dependencies explicit [\\#39](https://github.com/getbouncer/cardscan-demo-android/pull/39) ([awushensky](https://github.com/awushensky))\n- Display card pan always [\\#38](https://github.com/getbouncer/cardscan-demo-android/pull/38) ([awushensky](https://github.com/awushensky))\n- Bump kotlinx-coroutines-android from 1.3.6 to 1.3.7 [\\#19](https://github.com/getbouncer/scan-framework-android/pull/19) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump kotlinx-coroutines-test from 1.3.6 to 1.3.7 [\\#18](https://github.com/getbouncer/scan-framework-android/pull/18) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Use invalid api key for test [\\#14](https://github.com/getbouncer/scan-framework-android/pull/14) ([awushensky](https://github.com/awushensky))\n- Bump com.github.dcendents.android-maven from 2.0 to 2.1 [\\#13](https://github.com/getbouncer/scan-framework-android/pull/13) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump tensorflow-lite from 1.15.0 to 2.2.0 [\\#12](https://github.com/getbouncer/scan-framework-android/pull/12) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.jfrog.bintray from 1.7.3 to 1.8.5 [\\#11](https://github.com/getbouncer/scan-framework-android/pull/11) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add more tests [\\#8](https://github.com/getbouncer/scan-framework-android/pull/8) ([awushensky](https://github.com/awushensky))\n- Add android test github action [\\#7](https://github.com/getbouncer/scan-framework-android/pull/7) ([awushensky](https://github.com/awushensky))\n- Ensure results are not duplicated [\\#6](https://github.com/getbouncer/scan-framework-android/pull/6) ([awushensky](https://github.com/awushensky))\n- Optimize some image utilities [\\#5](https://github.com/getbouncer/scan-framework-android/pull/5) ([awushensky](https://github.com/awushensky))\n- Refocus camera [\\#15](https://github.com/getbouncer/scan-camera-android/pull/15) ([awushensky](https://github.com/awushensky))\n- Simplify camera start [\\#14](https://github.com/getbouncer/scan-camera-android/pull/14) ([awushensky](https://github.com/awushensky))\n- Ignore camera config change failures [\\#13](https://github.com/getbouncer/scan-camera-android/pull/13) ([awushensky](https://github.com/awushensky))\n- Bump kotlinx-coroutines-core from 1.3.6 to 1.3.7 [\\#12](https://github.com/getbouncer/scan-camera-android/pull/12) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump kotlinx-coroutines-test from 1.3.6 to 1.3.7 [\\#11](https://github.com/getbouncer/scan-camera-android/pull/11) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.github.dcendents.android-maven from 2.0 to 2.1 [\\#8](https://github.com/getbouncer/scan-camera-android/pull/8) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.jfrog.bintray from 1.7.3 to 1.8.5 [\\#7](https://github.com/getbouncer/scan-camera-android/pull/7) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add tests to camera [\\#5](https://github.com/getbouncer/scan-camera-android/pull/5) ([awushensky](https://github.com/awushensky))\n- Remove camerax and camera2 [\\#4](https://github.com/getbouncer/scan-camera-android/pull/4) ([awushensky](https://github.com/awushensky))\n- Add camera1 and camerax [\\#3](https://github.com/getbouncer/scan-camera-android/pull/3) ([awushensky](https://github.com/awushensky))\n- Use better coroutine testing [\\#13](https://github.com/getbouncer/scan-payment-android/pull/13) ([awushensky](https://github.com/awushensky))\n- Bump kotlinx-coroutines-core from 1.3.6 to 1.3.7 [\\#12](https://github.com/getbouncer/scan-payment-android/pull/12) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump kotlinx-coroutines-android from 1.3.6 to 1.3.7 [\\#11](https://github.com/getbouncer/scan-payment-android/pull/11) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.github.dcendents.android-maven from 2.0 to 2.1 [\\#10](https://github.com/getbouncer/scan-payment-android/pull/10) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.jfrog.bintray from 1.7.3 to 1.8.5 [\\#7](https://github.com/getbouncer/scan-payment-android/pull/7) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add tests [\\#5](https://github.com/getbouncer/scan-payment-android/pull/5) ([awushensky](https://github.com/awushensky))\n- Bump kotlinx-coroutines-core from 1.3.3 to 1.3.7 [\\#11](https://github.com/getbouncer/scan-ui-android/pull/11) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add tests [\\#10](https://github.com/getbouncer/scan-ui-android/pull/10) ([awushensky](https://github.com/awushensky))\n- Bump com.jfrog.bintray from 1.7.3 to 1.8.5 [\\#8](https://github.com/getbouncer/scan-ui-android/pull/8) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.github.dcendents.android-maven from 2.0 to 2.1 [\\#7](https://github.com/getbouncer/scan-ui-android/pull/7) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update default user interface [\\#6](https://github.com/getbouncer/scan-ui-android/pull/6) ([awushensky](https://github.com/awushensky))\n- Bump kotlinx-coroutines-core from 1.3.6 to 1.3.7 [\\#10](https://github.com/getbouncer/cardscan-ui-android/pull/10) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.jfrog.bintray from 1.7.3 to 1.8.5 [\\#8](https://github.com/getbouncer/cardscan-ui-android/pull/8) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.github.dcendents.android-maven from 2.0 to 2.1 [\\#7](https://github.com/getbouncer/cardscan-ui-android/pull/7) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add integration tests to CI [\\#6](https://github.com/getbouncer/cardscan-ui-android/pull/6) ([awushensky](https://github.com/awushensky))\n- Update user interface [\\#5](https://github.com/getbouncer/cardscan-ui-android/pull/5) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0007](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0007) (2020-05-12)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0006...2.0.0007)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0006...2.0.0007)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0006...2.0.0007)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0006...2.0.0007)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0006...2.0.0007)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0005...2.0.0007)\n\n**Merged pull requests:**\n\n- Update documentation [\\#37](https://github.com/getbouncer/cardscan-demo-android/pull/37) ([awushensky](https://github.com/awushensky))\n- Fix crash on network failure [\\#4](https://github.com/getbouncer/scan-framework-android/pull/4) ([awushensky](https://github.com/awushensky))\n- Fix crash on camera open failure [\\#2](https://github.com/getbouncer/scan-camera-android/pull/2) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#4](https://github.com/getbouncer/scan-payment-android/pull/4) ([awushensky](https://github.com/awushensky))\n- Update version [\\#5](https://github.com/getbouncer/scan-ui-android/pull/5) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#4](https://github.com/getbouncer/cardscan-ui-android/pull/4) ([awushensky](https://github.com/awushensky))\n- Set api key on warmup [\\#3](https://github.com/getbouncer/cardscan-ui-android/pull/3) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0006](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0006) (2020-05-09)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0005...2.0.0006)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0005...2.0.0006)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0005...2.0.0006)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0005...2.0.0006)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.005...2.0.0006)\n\n**Merged pull requests:**\n\n- Update version [\\#36](https://github.com/getbouncer/cardscan-demo-android/pull/36) ([awushensky](https://github.com/awushensky))\n- Fix signedUrl failure crash [\\#3](https://github.com/getbouncer/scan-framework-android/pull/3) ([awushensky](https://github.com/awushensky))\n- update version [\\#3](https://github.com/getbouncer/scan-payment-android/pull/3) ([awushensky](https://github.com/awushensky))\n- Allow invalid api key error [\\#4](https://github.com/getbouncer/scan-ui-android/pull/4) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0005](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0005) (2020-05-08)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0004...2.0.0005)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/e0e5089a945c8b6b6ed47f3838d3d61997c1afe4...2.0.0005)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/70e9702f2bb0a99ef89e1477064eb541b661170f...2.0.0005)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/d19502708ef72c01d522e2d18e7a8bfbb81ec0b2...2.0.0005)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/0904ef185c19491c25b73c61eb8a22bed1eecb75...2.0.005)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/ebb299b0e1b799ec7c12aff1f535d0278d9107c1...2.0.0005)\n\n**Merged pull requests:**\n\n- Update submodules [\\#35](https://github.com/getbouncer/cardscan-demo-android/pull/35) ([awushensky](https://github.com/awushensky))\n- Replace libraries [\\#34](https://github.com/getbouncer/cardscan-demo-android/pull/34) ([awushensky](https://github.com/awushensky))\n- Remove build from git [\\#2](https://github.com/getbouncer/scan-framework-android/pull/2) ([awushensky](https://github.com/awushensky))\n- Update documentation [\\#1](https://github.com/getbouncer/scan-framework-android/pull/1) ([awushensky](https://github.com/awushensky))\n- Update documentation [\\#1](https://github.com/getbouncer/scan-camera-android/pull/1) ([awushensky](https://github.com/awushensky))\n- Add results [\\#2](https://github.com/getbouncer/scan-payment-android/pull/2) ([awushensky](https://github.com/awushensky))\n- Update documentation [\\#1](https://github.com/getbouncer/scan-payment-android/pull/1) ([awushensky](https://github.com/awushensky))\n- Rename module in docs [\\#3](https://github.com/getbouncer/scan-ui-android/pull/3) ([awushensky](https://github.com/awushensky))\n- Rename module [\\#2](https://github.com/getbouncer/scan-ui-android/pull/2) ([awushensky](https://github.com/awushensky))\n- Update docs [\\#1](https://github.com/getbouncer/scan-ui-android/pull/1) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#2](https://github.com/getbouncer/cardscan-ui-android/pull/2) ([awushensky](https://github.com/awushensky))\n- Update documentation [\\#1](https://github.com/getbouncer/cardscan-ui-android/pull/1) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0004](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0004) (2020-05-06)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0003...2.0.0004)\n\n**Merged pull requests:**\n\n- Update version [\\#33](https://github.com/getbouncer/cardscan-demo-android/pull/33) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0003](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0003) (2020-04-29)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/a0e3c8e303b7f72e2b08701e01781d9558b07e2c...2.0.0003)\n\n**Implemented enhancements:**\n\n- Update github checks [\\#5](https://github.com/getbouncer/cardscan-demo-android/pull/5) ([awushensky](https://github.com/awushensky))\n\n**Merged pull requests:**\n\n- Update thread handling [\\#32](https://github.com/getbouncer/cardscan-demo-android/pull/32) ([awushensky](https://github.com/awushensky))\n- Update documentation [\\#31](https://github.com/getbouncer/cardscan-demo-android/pull/31) ([awushensky](https://github.com/awushensky))\n- Extract common ui [\\#30](https://github.com/getbouncer/cardscan-demo-android/pull/30) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#29](https://github.com/getbouncer/cardscan-demo-android/pull/29) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#28](https://github.com/getbouncer/cardscan-demo-android/pull/28) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#27](https://github.com/getbouncer/cardscan-demo-android/pull/27) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#26](https://github.com/getbouncer/cardscan-demo-android/pull/26) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#25](https://github.com/getbouncer/cardscan-demo-android/pull/25) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#24](https://github.com/getbouncer/cardscan-demo-android/pull/24) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#23](https://github.com/getbouncer/cardscan-demo-android/pull/23) ([awushensky](https://github.com/awushensky))\n- Prevent screenshots [\\#22](https://github.com/getbouncer/cardscan-demo-android/pull/22) ([awushensky](https://github.com/awushensky))\n- Add api key check [\\#21](https://github.com/getbouncer/cardscan-demo-android/pull/21) ([awushensky](https://github.com/awushensky))\n- Update license [\\#20](https://github.com/getbouncer/cardscan-demo-android/pull/20) ([awushensky](https://github.com/awushensky))\n- Update documentation [\\#19](https://github.com/getbouncer/cardscan-demo-android/pull/19) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#18](https://github.com/getbouncer/cardscan-demo-android/pull/18) ([awushensky](https://github.com/awushensky))\n- Add documentation [\\#17](https://github.com/getbouncer/cardscan-demo-android/pull/17) ([awushensky](https://github.com/awushensky))\n- Make logo optional [\\#16](https://github.com/getbouncer/cardscan-demo-android/pull/16) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#15](https://github.com/getbouncer/cardscan-demo-android/pull/15) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#14](https://github.com/getbouncer/cardscan-demo-android/pull/14) ([awushensky](https://github.com/awushensky))\n- Rename app to demo [\\#13](https://github.com/getbouncer/cardscan-demo-android/pull/13) ([awushensky](https://github.com/awushensky))\n- Support state in loops [\\#12](https://github.com/getbouncer/cardscan-demo-android/pull/12) ([awushensky](https://github.com/awushensky))\n- Remove camera 1 api [\\#11](https://github.com/getbouncer/cardscan-demo-android/pull/11) ([awushensky](https://github.com/awushensky))\n- Scope tests to the app itself [\\#10](https://github.com/getbouncer/cardscan-demo-android/pull/10) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#9](https://github.com/getbouncer/cardscan-demo-android/pull/9) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#8](https://github.com/getbouncer/cardscan-demo-android/pull/8) ([awushensky](https://github.com/awushensky))\n- Use submodules [\\#7](https://github.com/getbouncer/cardscan-demo-android/pull/7) ([awushensky](https://github.com/awushensky))\n- Show the card pan when scanninng [\\#6](https://github.com/getbouncer/cardscan-demo-android/pull/6) ([awushensky](https://github.com/awushensky))\n- Update dependencies [\\#4](https://github.com/getbouncer/cardscan-demo-android/pull/4) ([awushensky](https://github.com/awushensky))\n- Require API key [\\#3](https://github.com/getbouncer/cardscan-demo-android/pull/3) ([awushensky](https://github.com/awushensky))\n- Add code owners [\\#2](https://github.com/getbouncer/cardscan-demo-android/pull/2) ([awushensky](https://github.com/awushensky))\n- Support camera1 APIs [\\#1](https://github.com/getbouncer/cardscan-demo-android/pull/1) ([awushensky](https://github.com/awushensky))\n\n\n\n\\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "# This is a comment.\n# Each line is a file pattern followed by one or more owners.\n\n# These owners will be the default owners for everything in\n# the repo. Unless a later match takes precedence,\n# @global-owner1 and @global-owner2 will be requested for\n# review when someone opens a pull request.\n*       @awushensky @xsl @kingst @dxaen @awushensky-stripe\n\n# Order is important; the last matching pattern takes the most\n# precedence. When someone opens a pull request that only\n# modifies JS files, only @js-owner and not the global\n# owner(s) will be requested for a review.\n# *.js    @js-owner\n\n# You can also use email addresses if you prefer. They'll be\n# used to look up users just like we do for commit author\n# emails.\n# *.go docs@example.com\n\n# In this example, @doctocat owns any files in the build/logs\n# directory at the root of the repository and any of its\n# subdirectories.\n# /build/logs/ @doctocat\n\n# The `docs/*` pattern will match files like\n# `docs/getting-started.md` but not further nested files like\n# `docs/build-app/troubleshooting.md`.\n# docs/*  docs@example.com\n\n# In this example, @octocat owns any file in an apps directory\n# anywhere in your repository.\n# apps/ @octocat\n\n# In this example, @doctocat owns any file in the `/docs`\n# directory in the root of your repository.\n# /docs/ @doctocat\n"
  },
  {
    "path": "Contributor License Agreement",
    "content": "Bouncer\nIndividual and Entity Contributor License Agreement\n\nThank you for your interest in contributing to software projects managed by Bouncer Technologies,\nInc. (“We”, “Us” or “Our”). This Contributor License Agreement (“Agreement”) documents the rights\ngranted by contributors to Us. This Agreement is for your protection as a contributor as well as for\nour protection; it does not change your rights to use your own Contributions for any other purpose.\nTo make this document effective, please read the Agreement carefully and then (i) sign it and send\nit to Us by email (PDF) at bouncer-support@stripe.com or (ii) submit it to us electronically, in either\ncase by following the instructions at [insert hyperlink]. By signing this Agreement (including by\nclicking “I agree” and submitting it to us electronically), You are creating a legally binding\ncontract. If You are less than eighteen years old, please have Your parents or guardian sign the\nAgreement. This Agreement covers all future Contributions from You, and may cover more than one\nsoftware project managed by Us.\n\n1. Definitions\n--------------\n“Affiliates” means other Legal Entities that control, are controlled by, or under common control\nwith that Legal Entity. For the purposes of this definition, “control” means (i) the power, direct\nor indirect, to cause the direction or management of such Legal Entity, whether by contract or\notherwise, (ii) ownership of fifty percent (50%) or more of the outstanding shares or securities\nwhich vote to elect the management or other persons who direct such Legal Entity or (iii) beneficial\nownership of such entity.\n\n“Contribution” means any work of authorship that is Submitted by You to Us in which You own or\nassert ownership of the Copyright. By Submitting any Contribution, you represent that You own the\nCopyright in the entire work of authorship, or that you otherwise are legally entitled to Submit the\nContribution and to grant the licenses in this Agreement.\n\n“Copyright” means all rights protecting works of authorship owned or controlled by You or your\nAffiliates (as may be applicable), including copyright, moral and related (or neighboring) rights,\nas appropriate, for the full term of their existence, including any extensions by You.\n\n“Effective Date” means the date You execute this Agreement or the date You first Submit a\nContribution to Us, whichever is earlier.\n\n“Legal Entity” means an entity which is not a natural person.\n\n“Material” means the work of authorship which is made available by Us to third parties. When this\nAgreement covers more than one software project, the Material means the work of authorship to which\nthe Contribution was Submitted. After You Submit the Contribution, it may be included in the\nMaterial.\n\n“Media” means any portion of a Contribution which is not software.\n\n“Submit” means any form of electronic, verbal, or written communication sent to Us or our\nrepresentatives at a destination (including websites) that we own or control or that is otherwise\nregistered to us, including but not limited to electronic mailing lists, source code control\nsystems, instant messages or similar communications, and issue tracking systems that are managed by,\nor on behalf of, Us for the purpose of discussing and improving the Material, but excluding any\ncommunication that is conspicuously marked or otherwise designated in writing by You as “Not a\nContribution.”\n\n“Submission Date” means the date on which You Submit a Contribution to Us.\n\n“You” (entity). If You are an individual acting on your own behalf, then “You” means the individual\nwho Submits a Contribution to Us.\n\n“You” (individual). If You are Submitting any Contribution on behalf of any entity, then “You” means\nthe Legal Entity on behalf of whom you Submit a Contribution to Us.\n\n\n2. Grant of Rights\n------------------\n2.1 Copyright License\n(a) Except for the license granted to Us in this Agreement, You reserve all right, title, and\ninterest in and to Your Contributions. That means that you can keep doing whatever you want with\nyour Contribution, and you can license it to anyone you want under any terms you want.\n\n(b) To the maximum extent permitted by the relevant law, You grant to Us a perpetual, worldwide,\nnon-exclusive, transferable, no charge and royalty-free, irrevocable license under the Copyright\ncovering the Contribution, with the right to sublicense such rights through multiple tiers of\nsublicensees, to reproduce, modify, display, perform, sublicense and distribute the Contribution as\npart of the Material; provided that this license is subject to Section 2.3.\n\n2.2 Patent License\nFor patent claims including, without limitation, method, process, and apparatus claims which You (or\nyour Affiliates, as may be applicable) own, control or have the right to grant, now or in the\nfuture, You grant to Us a perpetual, worldwide, non-exclusive, transferable, no charge and royalty-\nfree, irrevocable patent license, with the right to sublicense these rights to multiple tiers of\nsublicensees, to make, have made, use, sell, offer for sale, import and otherwise transfer the\nContribution (and the Contribution in combination with the Material, and portions of such\ncombination). This license is granted only to the extent that the exercise of the licensed rights\ninfringes such patent claims; and is subject to Section 2.3. If any person institutes patent\nlitigation against Contributor or any other entity (including a cross-claim or counterclaim in a\nlawsuit) alleging that the Contributions, or the Project to which the Contributions were submitted,\nconstitutes direct or contributory patent infringement, then any patent licenses granted under this\nAgreement for that Contribution to the person or entity instituting the litigation, or the Project\nto which the Contributions were submitted, shall terminate as of the date such litigation is filed.\n\n2.3 Outbound License\nBased on the grant of rights in Sections 2.1 (meaning, no matter what, you can keep licensing your\nContribution to others however you want) and 2.2, if We include Your Contribution in any Material,\nand if We determine that it is appropriate for the purpose of commercializing any Material or any\nproject under Our control, we may license the Contribution under any license, including copyleft,\npermissive, commercial, or proprietary licenses.\n\n2.4 Moral Rights.\nWe agree to comply with applicable laws regarding your Contribution, including copyright laws and\nlaw related to moral rights. If moral rights apply to the Contribution, to the maximum extent\npermitted by law, You waive and agree not to assert such moral rights against Us or our successors\nin interest, or any of our licensees, either direct or indirect.\n\n2.5 Our Rights.\nYou acknowledge that We are not obligated to use Your Contribution as part of any Material, and that\nwe and may decide to include any Contribution We consider appropriate.\n\n2.6 Reservation of Rights.\nAny rights in Your Contribution not expressly licensed under this Agreement are expressly reserved\nby You.\n\n3. Agreement.\n-------------\nYou confirm that:\n\n(a) You have the legal authority to enter into this Agreement. If your employer(s) has rights to\nintellectual property that you create that includes your Contributions, you represent that you have\nreceived permission to make Contributions on behalf of that employer, and that your employer has\nwaived such rights for your Contributions to Us.\n\n(b) You (or your Affiliates, as may be applicable) own or otherwise have the legal right to license\nthe Copyright and patent claims covering the Contribution which are required to grant the rights\nunder Section 2.\n\n(c) The grant of rights under Section 2 does not violate any grant of rights which You (or your\nAffiliates, as may be applicable) have made to third parties, including Your employer.\n\n(d)  You represent that each of Your Contributions is Your original work. You represent that Your\nContribution submissions include complete details of any third-party license or other restriction\n(including, but not limited to, related patents and trademarks) of which you are personally aware\nand which are associated with any part of Your Contributions.\n\n(e) You agree to notify Us of any facts or circumstances of which you become aware that would make\nthese representations inaccurate in any respect.\n\n4. Disclaimer\n-------------\nEXCEPT FOR THE EXPRESS WARRANTIES IN SECTION 3, THE CONTRIBUTION IS PROVIDED \"AS IS\". YOU EXPRESSLY\nDISCLAIM ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. TO THE EXTENT THAT ANY SUCH\nWARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION TO THE MINIMUM PERIOD\nPERMITTED BY LAW.\n\n5. Consequential Damage Waiver\n------------------------------\nTO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU OR WE BE LIABLE FOR ANY LOSS\nOF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL\nAND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY\n(CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED.\n\n6. Miscellaneous\n----------------\n6.1 This Agreement will be governed by and construed in accordance with the laws of the State of\nCalifornia, without regard to conflicts of law provisions. The sole venue for all disputes relating\nto this Agreement shall be in Alameda County, California. The rights and obligations of the parties\nunder this Agreement shall not be governed by the 1980 U.N. Convention on Contracts for the\nInternational Sale of Goods.\n\n6.2 This Agreement may be amended only by a written document signed by the party against whom\nenforcement is sought.\n\n6.3 The failure of either party to require performance by the other party of any provision of this\nAgreement in one situation shall not affect the right of a party to require such performance at any\ntime in the future. A waiver of performance under a provision in one situation shall not be\nconsidered a waiver of the performance of the provision in the future or a waiver of the provision\nin its entirety.\n\n6.4 If any provision of this Agreement is found void and unenforceable, such provision will be\nreplaced to the extent possible with a provision that comes closest to the meaning of the original\nprovision and which is enforceable. The terms and conditions set forth in this Agreement shall apply\nnotwithstanding any failure of essential purpose of this Agreement or any limited remedy to the\nmaximum extent possible under law.\n\nThis Agreement contains the entire understanding of the parties regarding the subject matter of this\nAgreement and supersedes all prior and contemporaneous negotiations and agreements, whether written\nor oral, between the parties with respect to the subject matter of this Agreement.\n\nBy signing below, Contributor accepts and agrees to the preceding terms and conditions for\nContributor’s present and future Contributions submitted to Us.\n\n\n________________________________________\nSignature\n\n________________________________________\nContributor Name\n\n________________________________________\nLegal Entity Name (if applicable)\n\n________________________________________\nContributor Address\n\n________________________________________\nTitle\n\n________________________________________\nContributor Address\n\n________________________________________\nEmail\n\n________________________________________\nTelephone\n\n________________________________________\nDate\n"
  },
  {
    "path": "HISTORY.md",
    "content": "## [2.0.0014](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0014) (2020-07-14)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0013...2.0.0014)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0013...2.0.0014)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0013...2.0.0014)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0013...2.0.0014)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0013...2.0.0014)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0013...2.0.0014)\n\n**Merged pull requests:**\n\n- Image fragmentation [\\#53](https://github.com/getbouncer/scan-framework-android/pull/53) ([smkuhne](https://github.com/smkuhne))\n- Standardize models [\\#35](https://github.com/getbouncer/scan-payment-android/pull/35) ([awushensky](https://github.com/awushensky))\n- Image fragmentation [\\#33](https://github.com/getbouncer/scan-payment-android/pull/33) ([smkuhne](https://github.com/smkuhne))\n- Allow extending uploadstats [\\#30](https://github.com/getbouncer/scan-ui-android/pull/30) ([awushensky](https://github.com/awushensky))\n- Image fragmentation [\\#28](https://github.com/getbouncer/scan-ui-android/pull/28) ([smkuhne](https://github.com/smkuhne))\n\n## [2.0.0013](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0013) (2020-07-08)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0012...2.0.0013)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0012...2.0.0013)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0012...2.0.0013)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0012...2.0.0013)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0012...2.0.0013)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0012...2.0.0013)\n\n**Merged pull requests:**\n\n- Bump ktlint from 0.37.0 to 0.37.2 [\\#59](https://github.com/getbouncer/cardscan-demo-android/pull/59) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Use new warmup initializer [\\#58](https://github.com/getbouncer/cardscan-demo-android/pull/58) ([xsl](https://github.com/xsl))\n- Update UI [\\#57](https://github.com/getbouncer/cardscan-demo-android/pull/57) ([awushensky](https://github.com/awushensky))\n- Update for expiry extraction + new text detector [\\#56](https://github.com/getbouncer/cardscan-demo-android/pull/56) ([xsl](https://github.com/xsl))\n- Add more java interoperability [\\#57](https://github.com/getbouncer/scan-framework-android/pull/57) ([awushensky](https://github.com/awushensky))\n- Relocate loop state to result [\\#56](https://github.com/getbouncer/scan-framework-android/pull/56) ([awushensky](https://github.com/awushensky))\n- Add more memoize functions [\\#55](https://github.com/getbouncer/scan-framework-android/pull/55) ([awushensky](https://github.com/awushensky))\n- Separate error listeners [\\#54](https://github.com/getbouncer/scan-framework-android/pull/54) ([awushensky](https://github.com/awushensky))\n- Fix work leak [\\#52](https://github.com/getbouncer/scan-framework-android/pull/52) ([awushensky](https://github.com/awushensky))\n- Test duration [\\#51](https://github.com/getbouncer/scan-framework-android/pull/51) ([awushensky](https://github.com/awushensky))\n- Clean up network responses [\\#49](https://github.com/getbouncer/scan-framework-android/pull/49) ([awushensky](https://github.com/awushensky))\n- Add exceptions to retry [\\#48](https://github.com/getbouncer/scan-framework-android/pull/48) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.37.0 to 0.37.2 [\\#46](https://github.com/getbouncer/scan-framework-android/pull/46) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add device identifier to network requests [\\#45](https://github.com/getbouncer/scan-framework-android/pull/45) ([awushensky](https://github.com/awushensky))\n- Standardize local file name [\\#44](https://github.com/getbouncer/scan-framework-android/pull/44) ([awushensky](https://github.com/awushensky))\n- Add some convenience functions [\\#43](https://github.com/getbouncer/scan-framework-android/pull/43) ([xsl](https://github.com/xsl))\n- Launch the camera on IO dispatcher [\\#32](https://github.com/getbouncer/scan-camera-android/pull/32) ([awushensky](https://github.com/awushensky))\n- Relocate analyzers [\\#34](https://github.com/getbouncer/scan-payment-android/pull/34) ([awushensky](https://github.com/awushensky))\n- Support multiple MM/YYs on cards [\\#32](https://github.com/getbouncer/scan-payment-android/pull/32) ([xsl](https://github.com/xsl))\n- Bump ktlint from 0.37.0 to 0.37.2 [\\#31](https://github.com/getbouncer/scan-payment-android/pull/31) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Expiry extraction + new text detector [\\#30](https://github.com/getbouncer/scan-payment-android/pull/30) ([xsl](https://github.com/xsl))\n- Update submodule [\\#29](https://github.com/getbouncer/scan-ui-android/pull/29) ([awushensky](https://github.com/awushensky))\n- Separate scan stats [\\#27](https://github.com/getbouncer/scan-ui-android/pull/27) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.37.0 to 0.37.2 [\\#26](https://github.com/getbouncer/scan-ui-android/pull/26) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add initialization error strings [\\#25](https://github.com/getbouncer/scan-ui-android/pull/25) ([xsl](https://github.com/xsl))\n- Update UI [\\#24](https://github.com/getbouncer/scan-ui-android/pull/24) ([awushensky](https://github.com/awushensky))\n- Update scan-framework-android submodule [\\#23](https://github.com/getbouncer/scan-ui-android/pull/23) ([xsl](https://github.com/xsl))\n- Shut down analyzer context on quit [\\#38](https://github.com/getbouncer/cardscan-ui-android/pull/38) ([awushensky](https://github.com/awushensky))\n- Separate loop logic [\\#37](https://github.com/getbouncer/cardscan-ui-android/pull/37) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.37.0 to 0.37.2 [\\#35](https://github.com/getbouncer/cardscan-ui-android/pull/35) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update ui [\\#34](https://github.com/getbouncer/cardscan-ui-android/pull/34) ([awushensky](https://github.com/awushensky))\n- Warmup name and expiry [\\#33](https://github.com/getbouncer/cardscan-ui-android/pull/33) ([xsl](https://github.com/xsl))\n- Expiry extraction + new text detector [\\#32](https://github.com/getbouncer/cardscan-ui-android/pull/32) ([xsl](https://github.com/xsl))\n\n## [2.0.0012](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0012) (2020-06-15)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0011...2.0.0012)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0011...2.0.0012)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0011...2.0.0012)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0011...2.0.0012)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0011...2.0.0012)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0011...2.0.0012)\n\n**Merged pull requests:**\n\n- Rename warm up [\\#55](https://github.com/getbouncer/cardscan-demo-android/pull/55) ([awushensky](https://github.com/awushensky))\n- Use flows instead of channels [\\#42](https://github.com/getbouncer/scan-framework-android/pull/42) ([awushensky](https://github.com/awushensky))\n- Use channels better [\\#41](https://github.com/getbouncer/scan-framework-android/pull/41) ([awushensky](https://github.com/awushensky))\n- Fix framerate average calculation [\\#40](https://github.com/getbouncer/scan-framework-android/pull/40) ([awushensky](https://github.com/awushensky))\n- Make result handlers listen to a lifecycle [\\#39](https://github.com/getbouncer/scan-framework-android/pull/39) ([awushensky](https://github.com/awushensky))\n- Move images out of framework [\\#38](https://github.com/getbouncer/scan-framework-android/pull/38) ([awushensky](https://github.com/awushensky))\n- Support java interop [\\#37](https://github.com/getbouncer/scan-framework-android/pull/37) ([awushensky](https://github.com/awushensky))\n- Fix camera crash on flash not supported [\\#30](https://github.com/getbouncer/scan-camera-android/pull/30) ([awushensky](https://github.com/awushensky))\n- Increase the channel buffer size to 2 [\\#29](https://github.com/getbouncer/scan-camera-android/pull/29) ([awushensky](https://github.com/awushensky))\n- Reintroduce camera2 [\\#28](https://github.com/getbouncer/scan-camera-android/pull/28) ([awushensky](https://github.com/awushensky))\n- Centralize the channel logic [\\#27](https://github.com/getbouncer/scan-camera-android/pull/27) ([awushensky](https://github.com/awushensky))\n- Remove framework submodule [\\#26](https://github.com/getbouncer/scan-camera-android/pull/26) ([awushensky](https://github.com/awushensky))\n- Add timing to ssdocr input [\\#29](https://github.com/getbouncer/scan-payment-android/pull/29) ([awushensky](https://github.com/awushensky))\n- Relocate test resources [\\#28](https://github.com/getbouncer/scan-payment-android/pull/28) ([awushensky](https://github.com/awushensky))\n- Relocate image manipulation utilities [\\#27](https://github.com/getbouncer/scan-payment-android/pull/27) ([awushensky](https://github.com/awushensky))\n- Use flows [\\#22](https://github.com/getbouncer/scan-ui-android/pull/22) ([awushensky](https://github.com/awushensky))\n- Relocate scan process [\\#21](https://github.com/getbouncer/scan-ui-android/pull/21) ([awushensky](https://github.com/awushensky))\n- Separate framework from camera [\\#20](https://github.com/getbouncer/scan-ui-android/pull/20) ([awushensky](https://github.com/awushensky))\n- Handle devices without cameras [\\#19](https://github.com/getbouncer/scan-ui-android/pull/19) ([awushensky](https://github.com/awushensky))\n- Reduce jitter in name display [\\#31](https://github.com/getbouncer/cardscan-ui-android/pull/31) ([awushensky](https://github.com/awushensky))\n- Fix analyzer pool memory leak [\\#30](https://github.com/getbouncer/cardscan-ui-android/pull/30) ([awushensky](https://github.com/awushensky))\n- Actually reset the result aggregator [\\#29](https://github.com/getbouncer/cardscan-ui-android/pull/29) ([awushensky](https://github.com/awushensky))\n- Relocate scan logic [\\#28](https://github.com/getbouncer/cardscan-ui-android/pull/28) ([awushensky](https://github.com/awushensky))\n- Relocate scan flow to implementation [\\#27](https://github.com/getbouncer/cardscan-ui-android/pull/27) ([awushensky](https://github.com/awushensky))\n- Separate camera and loops [\\#26](https://github.com/getbouncer/cardscan-ui-android/pull/26) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0011](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0011) (2020-06-08)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0009...2.0.0011)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0010...2.0.0011)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0010...2.0.0011)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0010...2.0.0011)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0010...2.0.0011)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0009...2.0.0011)\n\n**Merged pull requests:**\n\n- Support separate name extraction parameters [\\#54](https://github.com/getbouncer/cardscan-demo-android/pull/54) ([awushensky](https://github.com/awushensky))\n- Add docs and buttons for name extraction [\\#53](https://github.com/getbouncer/cardscan-demo-android/pull/53) ([xsl](https://github.com/xsl))\n- Reduce name extraction settings [\\#52](https://github.com/getbouncer/cardscan-demo-android/pull/52) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.36.0 to 0.37.0 [\\#51](https://github.com/getbouncer/cardscan-demo-android/pull/51) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update for name extraction [\\#50](https://github.com/getbouncer/cardscan-demo-android/pull/50) ([xsl](https://github.com/xsl))\n- Bump gradle from 3.6.3 to 4.0.0 [\\#47](https://github.com/getbouncer/cardscan-demo-android/pull/47) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update coroutine interoperability [\\#36](https://github.com/getbouncer/scan-framework-android/pull/36) ([awushensky](https://github.com/awushensky))\n- Standardize result counter [\\#35](https://github.com/getbouncer/scan-framework-android/pull/35) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.36.0 to 0.37.0 [\\#34](https://github.com/getbouncer/scan-framework-android/pull/34) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add memoization functions [\\#33](https://github.com/getbouncer/scan-framework-android/pull/33) ([awushensky](https://github.com/awushensky))\n- Don't retry on FileNotFoundExceptions [\\#32](https://github.com/getbouncer/scan-framework-android/pull/32) ([xsl](https://github.com/xsl))\n- Update image utilities [\\#31](https://github.com/getbouncer/scan-framework-android/pull/31) ([awushensky](https://github.com/awushensky))\n- Use default dispatcher for camera [\\#25](https://github.com/getbouncer/scan-camera-android/pull/25) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.36.0 to 0.37.0 [\\#24](https://github.com/getbouncer/scan-camera-android/pull/24) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update submodule [\\#23](https://github.com/getbouncer/scan-camera-android/pull/23) ([xsl](https://github.com/xsl))\n- Update image utils [\\#22](https://github.com/getbouncer/scan-camera-android/pull/22) ([awushensky](https://github.com/awushensky))\n- Remove threadsafe flag [\\#26](https://github.com/getbouncer/scan-payment-android/pull/26) ([awushensky](https://github.com/awushensky))\n- Minor tuning for name extraction [\\#25](https://github.com/getbouncer/scan-payment-android/pull/25) ([xsl](https://github.com/xsl))\n- Relocate object detect test [\\#24](https://github.com/getbouncer/scan-payment-android/pull/24) ([awushensky](https://github.com/awushensky))\n- Remove debug log [\\#23](https://github.com/getbouncer/scan-payment-android/pull/23) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.36.0 to 0.37.0 [\\#22](https://github.com/getbouncer/scan-payment-android/pull/22) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update image utils [\\#21](https://github.com/getbouncer/scan-payment-android/pull/21) ([awushensky](https://github.com/awushensky))\n- Add NameDetectAnalyzer and move object detector over \\(for now at least\\) [\\#20](https://github.com/getbouncer/scan-payment-android/pull/20) ([xsl](https://github.com/xsl))\n- Remove unnecessary manifest entries [\\#18](https://github.com/getbouncer/scan-ui-android/pull/18) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.36.0 to 0.37.0 [\\#17](https://github.com/getbouncer/scan-ui-android/pull/17) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Introduce fadeIn duration config and update card border animation duration [\\#16](https://github.com/getbouncer/scan-ui-android/pull/16) ([xsl](https://github.com/xsl))\n- Support java interoperability [\\#25](https://github.com/getbouncer/cardscan-ui-android/pull/25) ([awushensky](https://github.com/awushensky))\n- Enable disabling name extraction on start [\\#24](https://github.com/getbouncer/cardscan-ui-android/pull/24) ([awushensky](https://github.com/awushensky))\n- Update scan payments submodule [\\#23](https://github.com/getbouncer/cardscan-ui-android/pull/23) ([xsl](https://github.com/xsl))\n- Use default dispatchers [\\#22](https://github.com/getbouncer/cardscan-ui-android/pull/22) ([awushensky](https://github.com/awushensky))\n- Reduce settings for name extractor [\\#21](https://github.com/getbouncer/cardscan-ui-android/pull/21) ([awushensky](https://github.com/awushensky))\n- Bump ktlint from 0.36.0 to 0.37.0 [\\#20](https://github.com/getbouncer/cardscan-ui-android/pull/20) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update image utils [\\#19](https://github.com/getbouncer/cardscan-ui-android/pull/19) ([awushensky](https://github.com/awushensky))\n- Name extraction v1 w/ old object detector [\\#18](https://github.com/getbouncer/cardscan-ui-android/pull/18) ([xsl](https://github.com/xsl))\n- Bump gradle from 3.6.3 to 4.0.0 [\\#17](https://github.com/getbouncer/cardscan-ui-android/pull/17) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0010](https://github.com/getbouncer/scan-framework-android/tree/2.0.0010) (2020-05-30)\n\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0009...2.0.0010)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0009...2.0.0010)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0009...2.0.0010)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0009...2.0.0010)\n\n**Merged pull requests:**\n\n- Terminate a finite loop that has no data [\\#30](https://github.com/getbouncer/scan-framework-android/pull/30) ([awushensky](https://github.com/awushensky))\n- Bump gradle from 3.6.3 to 4.0.0 [\\#29](https://github.com/getbouncer/scan-framework-android/pull/29) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Better support camera flash [\\#21](https://github.com/getbouncer/scan-camera-android/pull/21) ([awushensky](https://github.com/awushensky))\n- Bump gradle from 3.6.3 to 4.0.0 [\\#20](https://github.com/getbouncer/scan-camera-android/pull/20) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump gradle from 3.6.3 to 4.0.0 [\\#19](https://github.com/getbouncer/scan-payment-android/pull/19) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump gradle from 3.6.3 to 4.0.0 [\\#15](https://github.com/getbouncer/scan-ui-android/pull/15) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n\n## [2.0.0009](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0009) (2020-05-29)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0008...2.0.0009)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0008...2.0.0009)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0008...2.0.0009)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0008...2.0.0009)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0008...2.0.0009)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0008...2.0.0009)\n\n**Merged pull requests:**\n\n- Bump core-ktx from 1.2.0 to 1.3.0 [\\#46](https://github.com/getbouncer/cardscan-demo-android/pull/46) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Remove required card number [\\#45](https://github.com/getbouncer/cardscan-demo-android/pull/45) ([awushensky](https://github.com/awushensky))\n- Bump core-ktx from 1.2.0 to 1.3.0 [\\#28](https://github.com/getbouncer/scan-framework-android/pull/28) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Start aggregation timer on valid result [\\#27](https://github.com/getbouncer/scan-framework-android/pull/27) ([awushensky](https://github.com/awushensky))\n- Stop ignoring scan timeout [\\#26](https://github.com/getbouncer/scan-framework-android/pull/26) ([awushensky](https://github.com/awushensky))\n- Simplify results [\\#25](https://github.com/getbouncer/scan-framework-android/pull/25) ([awushensky](https://github.com/awushensky))\n- Add logging to stats [\\#24](https://github.com/getbouncer/scan-framework-android/pull/24) ([awushensky](https://github.com/awushensky))\n- Bump core-ktx from 1.2.0 to 1.3.0 [\\#19](https://github.com/getbouncer/scan-camera-android/pull/19) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Use random focus variance [\\#18](https://github.com/getbouncer/scan-camera-android/pull/18) ([awushensky](https://github.com/awushensky))\n- Bump core-ktx from 1.2.0 to 1.3.0 [\\#18](https://github.com/getbouncer/scan-payment-android/pull/18) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Relocate aggregator [\\#17](https://github.com/getbouncer/scan-payment-android/pull/17) ([awushensky](https://github.com/awushensky))\n- Bump core-ktx from 1.2.0 to 1.3.0 [\\#14](https://github.com/getbouncer/scan-ui-android/pull/14) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Keep the screen on while scanning [\\#13](https://github.com/getbouncer/scan-ui-android/pull/13) ([awushensky](https://github.com/awushensky))\n- Reset previously valid result [\\#16](https://github.com/getbouncer/cardscan-ui-android/pull/16) ([awushensky](https://github.com/awushensky))\n- Bump core-ktx from 1.2.0 to 1.3.0 [\\#15](https://github.com/getbouncer/cardscan-ui-android/pull/15) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Remove isValidPan [\\#14](https://github.com/getbouncer/cardscan-ui-android/pull/14) ([awushensky](https://github.com/awushensky))\n- Start result aggregation on valid result [\\#13](https://github.com/getbouncer/cardscan-ui-android/pull/13) ([awushensky](https://github.com/awushensky))\n- Simplify results [\\#12](https://github.com/getbouncer/cardscan-ui-android/pull/12) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0008](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0008) (2020-05-21)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0007...2.0.0008)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0007...2.0.0008)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0007...2.0.0008)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0007...2.0.0008)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0007...2.0.0008)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0007...2.0.0008)\n\n**Merged pull requests:**\n\n- Bump kotlinx-coroutines-core from 1.3.6 to 1.3.7 [\\#43](https://github.com/getbouncer/cardscan-demo-android/pull/43) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.jfrog.bintray from 1.7.3 to 1.8.5 [\\#42](https://github.com/getbouncer/cardscan-demo-android/pull/42) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.github.dcendents.android-maven from 2.0 to 2.1 [\\#41](https://github.com/getbouncer/cardscan-demo-android/pull/41) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Make dependencies explicit [\\#39](https://github.com/getbouncer/cardscan-demo-android/pull/39) ([awushensky](https://github.com/awushensky))\n- Display card pan always [\\#38](https://github.com/getbouncer/cardscan-demo-android/pull/38) ([awushensky](https://github.com/awushensky))\n- Bump kotlinx-coroutines-android from 1.3.6 to 1.3.7 [\\#19](https://github.com/getbouncer/scan-framework-android/pull/19) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump kotlinx-coroutines-test from 1.3.6 to 1.3.7 [\\#18](https://github.com/getbouncer/scan-framework-android/pull/18) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Use invalid api key for test [\\#14](https://github.com/getbouncer/scan-framework-android/pull/14) ([awushensky](https://github.com/awushensky))\n- Bump com.github.dcendents.android-maven from 2.0 to 2.1 [\\#13](https://github.com/getbouncer/scan-framework-android/pull/13) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump tensorflow-lite from 1.15.0 to 2.2.0 [\\#12](https://github.com/getbouncer/scan-framework-android/pull/12) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.jfrog.bintray from 1.7.3 to 1.8.5 [\\#11](https://github.com/getbouncer/scan-framework-android/pull/11) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add more tests [\\#8](https://github.com/getbouncer/scan-framework-android/pull/8) ([awushensky](https://github.com/awushensky))\n- Add android test github action [\\#7](https://github.com/getbouncer/scan-framework-android/pull/7) ([awushensky](https://github.com/awushensky))\n- Ensure results are not duplicated [\\#6](https://github.com/getbouncer/scan-framework-android/pull/6) ([awushensky](https://github.com/awushensky))\n- Optimize some image utilities [\\#5](https://github.com/getbouncer/scan-framework-android/pull/5) ([awushensky](https://github.com/awushensky))\n- Refocus camera [\\#15](https://github.com/getbouncer/scan-camera-android/pull/15) ([awushensky](https://github.com/awushensky))\n- Simplify camera start [\\#14](https://github.com/getbouncer/scan-camera-android/pull/14) ([awushensky](https://github.com/awushensky))\n- Ignore camera config change failures [\\#13](https://github.com/getbouncer/scan-camera-android/pull/13) ([awushensky](https://github.com/awushensky))\n- Bump kotlinx-coroutines-core from 1.3.6 to 1.3.7 [\\#12](https://github.com/getbouncer/scan-camera-android/pull/12) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump kotlinx-coroutines-test from 1.3.6 to 1.3.7 [\\#11](https://github.com/getbouncer/scan-camera-android/pull/11) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.github.dcendents.android-maven from 2.0 to 2.1 [\\#8](https://github.com/getbouncer/scan-camera-android/pull/8) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.jfrog.bintray from 1.7.3 to 1.8.5 [\\#7](https://github.com/getbouncer/scan-camera-android/pull/7) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add tests to camera [\\#5](https://github.com/getbouncer/scan-camera-android/pull/5) ([awushensky](https://github.com/awushensky))\n- Remove camerax and camera2 [\\#4](https://github.com/getbouncer/scan-camera-android/pull/4) ([awushensky](https://github.com/awushensky))\n- Add camera1 and camerax [\\#3](https://github.com/getbouncer/scan-camera-android/pull/3) ([awushensky](https://github.com/awushensky))\n- Use better coroutine testing [\\#13](https://github.com/getbouncer/scan-payment-android/pull/13) ([awushensky](https://github.com/awushensky))\n- Bump kotlinx-coroutines-core from 1.3.6 to 1.3.7 [\\#12](https://github.com/getbouncer/scan-payment-android/pull/12) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump kotlinx-coroutines-android from 1.3.6 to 1.3.7 [\\#11](https://github.com/getbouncer/scan-payment-android/pull/11) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.github.dcendents.android-maven from 2.0 to 2.1 [\\#10](https://github.com/getbouncer/scan-payment-android/pull/10) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.jfrog.bintray from 1.7.3 to 1.8.5 [\\#7](https://github.com/getbouncer/scan-payment-android/pull/7) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add tests [\\#5](https://github.com/getbouncer/scan-payment-android/pull/5) ([awushensky](https://github.com/awushensky))\n- Bump kotlinx-coroutines-core from 1.3.3 to 1.3.7 [\\#11](https://github.com/getbouncer/scan-ui-android/pull/11) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add tests [\\#10](https://github.com/getbouncer/scan-ui-android/pull/10) ([awushensky](https://github.com/awushensky))\n- Bump com.jfrog.bintray from 1.7.3 to 1.8.5 [\\#8](https://github.com/getbouncer/scan-ui-android/pull/8) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.github.dcendents.android-maven from 2.0 to 2.1 [\\#7](https://github.com/getbouncer/scan-ui-android/pull/7) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Update default user interface [\\#6](https://github.com/getbouncer/scan-ui-android/pull/6) ([awushensky](https://github.com/awushensky))\n- Bump kotlinx-coroutines-core from 1.3.6 to 1.3.7 [\\#10](https://github.com/getbouncer/cardscan-ui-android/pull/10) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.jfrog.bintray from 1.7.3 to 1.8.5 [\\#8](https://github.com/getbouncer/cardscan-ui-android/pull/8) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Bump com.github.dcendents.android-maven from 2.0 to 2.1 [\\#7](https://github.com/getbouncer/cardscan-ui-android/pull/7) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))\n- Add integration tests to CI [\\#6](https://github.com/getbouncer/cardscan-ui-android/pull/6) ([awushensky](https://github.com/awushensky))\n- Update user interface [\\#5](https://github.com/getbouncer/cardscan-ui-android/pull/5) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0007](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0007) (2020-05-12)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0006...2.0.0007)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0006...2.0.0007)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0006...2.0.0007)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0006...2.0.0007)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0006...2.0.0007)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0005...2.0.0007)\n\n**Merged pull requests:**\n\n- Update documentation [\\#37](https://github.com/getbouncer/cardscan-demo-android/pull/37) ([awushensky](https://github.com/awushensky))\n- Fix crash on network failure [\\#4](https://github.com/getbouncer/scan-framework-android/pull/4) ([awushensky](https://github.com/awushensky))\n- Fix crash on camera open failure [\\#2](https://github.com/getbouncer/scan-camera-android/pull/2) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#4](https://github.com/getbouncer/scan-payment-android/pull/4) ([awushensky](https://github.com/awushensky))\n- Update version [\\#5](https://github.com/getbouncer/scan-ui-android/pull/5) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#4](https://github.com/getbouncer/cardscan-ui-android/pull/4) ([awushensky](https://github.com/awushensky))\n- Set api key on warmup [\\#3](https://github.com/getbouncer/cardscan-ui-android/pull/3) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0006](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0006) (2020-05-09)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0005...2.0.0006)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0005...2.0.0006)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0005...2.0.0006)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0005...2.0.0006)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.005...2.0.0006)\n\n**Merged pull requests:**\n\n- Update version [\\#36](https://github.com/getbouncer/cardscan-demo-android/pull/36) ([awushensky](https://github.com/awushensky))\n- Fix signedUrl failure crash [\\#3](https://github.com/getbouncer/scan-framework-android/pull/3) ([awushensky](https://github.com/awushensky))\n- update version [\\#3](https://github.com/getbouncer/scan-payment-android/pull/3) ([awushensky](https://github.com/awushensky))\n- Allow invalid api key error [\\#4](https://github.com/getbouncer/scan-ui-android/pull/4) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0005](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0005) (2020-05-08)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0004...2.0.0005)\n[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/e0e5089a945c8b6b6ed47f3838d3d61997c1afe4...2.0.0005)\n[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/70e9702f2bb0a99ef89e1477064eb541b661170f...2.0.0005)\n[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/d19502708ef72c01d522e2d18e7a8bfbb81ec0b2...2.0.0005)\n[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/0904ef185c19491c25b73c61eb8a22bed1eecb75...2.0.005)\n[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/ebb299b0e1b799ec7c12aff1f535d0278d9107c1...2.0.0005)\n\n**Merged pull requests:**\n\n- Update submodules [\\#35](https://github.com/getbouncer/cardscan-demo-android/pull/35) ([awushensky](https://github.com/awushensky))\n- Replace libraries [\\#34](https://github.com/getbouncer/cardscan-demo-android/pull/34) ([awushensky](https://github.com/awushensky))\n- Remove build from git [\\#2](https://github.com/getbouncer/scan-framework-android/pull/2) ([awushensky](https://github.com/awushensky))\n- Update documentation [\\#1](https://github.com/getbouncer/scan-framework-android/pull/1) ([awushensky](https://github.com/awushensky))\n- Update documentation [\\#1](https://github.com/getbouncer/scan-camera-android/pull/1) ([awushensky](https://github.com/awushensky))\n- Add results [\\#2](https://github.com/getbouncer/scan-payment-android/pull/2) ([awushensky](https://github.com/awushensky))\n- Update documentation [\\#1](https://github.com/getbouncer/scan-payment-android/pull/1) ([awushensky](https://github.com/awushensky))\n- Rename module in docs [\\#3](https://github.com/getbouncer/scan-ui-android/pull/3) ([awushensky](https://github.com/awushensky))\n- Rename module [\\#2](https://github.com/getbouncer/scan-ui-android/pull/2) ([awushensky](https://github.com/awushensky))\n- Update docs [\\#1](https://github.com/getbouncer/scan-ui-android/pull/1) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#2](https://github.com/getbouncer/cardscan-ui-android/pull/2) ([awushensky](https://github.com/awushensky))\n- Update documentation [\\#1](https://github.com/getbouncer/cardscan-ui-android/pull/1) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0004](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0004) (2020-05-06)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0003...2.0.0004)\n\n**Merged pull requests:**\n\n- Update version [\\#33](https://github.com/getbouncer/cardscan-demo-android/pull/33) ([awushensky](https://github.com/awushensky))\n\n## [2.0.0003](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0003) (2020-04-29)\n\n[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/a0e3c8e303b7f72e2b08701e01781d9558b07e2c...2.0.0003)\n\n**Implemented enhancements:**\n\n- Update github checks [\\#5](https://github.com/getbouncer/cardscan-demo-android/pull/5) ([awushensky](https://github.com/awushensky))\n\n**Merged pull requests:**\n\n- Update thread handling [\\#32](https://github.com/getbouncer/cardscan-demo-android/pull/32) ([awushensky](https://github.com/awushensky))\n- Update documentation [\\#31](https://github.com/getbouncer/cardscan-demo-android/pull/31) ([awushensky](https://github.com/awushensky))\n- Extract common ui [\\#30](https://github.com/getbouncer/cardscan-demo-android/pull/30) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#29](https://github.com/getbouncer/cardscan-demo-android/pull/29) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#28](https://github.com/getbouncer/cardscan-demo-android/pull/28) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#27](https://github.com/getbouncer/cardscan-demo-android/pull/27) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#26](https://github.com/getbouncer/cardscan-demo-android/pull/26) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#25](https://github.com/getbouncer/cardscan-demo-android/pull/25) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#24](https://github.com/getbouncer/cardscan-demo-android/pull/24) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#23](https://github.com/getbouncer/cardscan-demo-android/pull/23) ([awushensky](https://github.com/awushensky))\n- Prevent screenshots [\\#22](https://github.com/getbouncer/cardscan-demo-android/pull/22) ([awushensky](https://github.com/awushensky))\n- Add api key check [\\#21](https://github.com/getbouncer/cardscan-demo-android/pull/21) ([awushensky](https://github.com/awushensky))\n- Update license [\\#20](https://github.com/getbouncer/cardscan-demo-android/pull/20) ([awushensky](https://github.com/awushensky))\n- Update documentation [\\#19](https://github.com/getbouncer/cardscan-demo-android/pull/19) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#18](https://github.com/getbouncer/cardscan-demo-android/pull/18) ([awushensky](https://github.com/awushensky))\n- Add documentation [\\#17](https://github.com/getbouncer/cardscan-demo-android/pull/17) ([awushensky](https://github.com/awushensky))\n- Make logo optional [\\#16](https://github.com/getbouncer/cardscan-demo-android/pull/16) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#15](https://github.com/getbouncer/cardscan-demo-android/pull/15) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#14](https://github.com/getbouncer/cardscan-demo-android/pull/14) ([awushensky](https://github.com/awushensky))\n- Rename app to demo [\\#13](https://github.com/getbouncer/cardscan-demo-android/pull/13) ([awushensky](https://github.com/awushensky))\n- Support state in loops [\\#12](https://github.com/getbouncer/cardscan-demo-android/pull/12) ([awushensky](https://github.com/awushensky))\n- Remove camera 1 api [\\#11](https://github.com/getbouncer/cardscan-demo-android/pull/11) ([awushensky](https://github.com/awushensky))\n- Scope tests to the app itself [\\#10](https://github.com/getbouncer/cardscan-demo-android/pull/10) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#9](https://github.com/getbouncer/cardscan-demo-android/pull/9) ([awushensky](https://github.com/awushensky))\n- Update submodules [\\#8](https://github.com/getbouncer/cardscan-demo-android/pull/8) ([awushensky](https://github.com/awushensky))\n- Use submodules [\\#7](https://github.com/getbouncer/cardscan-demo-android/pull/7) ([awushensky](https://github.com/awushensky))\n- Show the card pan when scanninng [\\#6](https://github.com/getbouncer/cardscan-demo-android/pull/6) ([awushensky](https://github.com/awushensky))\n- Update dependencies [\\#4](https://github.com/getbouncer/cardscan-demo-android/pull/4) ([awushensky](https://github.com/awushensky))\n- Require API key [\\#3](https://github.com/getbouncer/cardscan-demo-android/pull/3) ([awushensky](https://github.com/awushensky))\n- Add code owners [\\#2](https://github.com/getbouncer/cardscan-demo-android/pull/2) ([awushensky](https://github.com/awushensky))\n- Support camera1 APIs [\\#1](https://github.com/getbouncer/cardscan-demo-android/pull/1) ([awushensky](https://github.com/awushensky))\n\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License\n\nCopyright (c) 2011- Stripe, Inc. (https://stripe.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Deprecation Notice\nHello from the Stripe (formerly Bouncer) team!\n\nWe're excited to provide an update on the state and future of the [Card Scan OCR](https://github.com/stripe/stripe-android/tree/master/stripecardscan) product! As we continue to build into Stripe's ecosystem, we'll be supporting the mission to continuously improve the end customer experience in many of Stripe's core checkout products.\n\nThis SDK has been [migrated to Stripe](https://github.com/stripe/stripe-android/tree/master/stripecardscan) and is now free for use under the MIT license!\n\nIf you are not currently a Stripe user, and interested in learning more about improving checkout experience through Stripe, please let us know and we can connect you with the team.\n\nIf you are not currently a Stripe user, and want to continue using the existing SDK, you can do so free of charge. Starting January 1, 2022, we will no longer be charging for use of the existing Bouncer Card Scan OCR SDK. For product support on [Android](https://github.com/stripe/stripe-android/issues) and [iOS](https://github.com/stripe/stripe-ios/issues). For billing support, please email [bouncer-support@stripe.com](mailto:bouncer-support@stripe.com).\n\nFor the new product, please visit the [stripe github repository](https://github.com/stripe/stripe-android/tree/master/stripecardscan).\n\n# Overview\nThis repository contains the legacy, deprecated open source code for [Bouncer](https://www.getbouncer.com) products (e.g. CardScan). See the individual sub modules for more information on each.\n\n[CardScan](https://getbouncer.com/scan) is a relatively small library that provides fast and accurate payment card scanning.\n\nCardScan is the foundation for CardVerify enterprise libraries, which validate the authenticity of payment cards as they are scanned.\n\n![Lint](https://github.com/getbouncer/cardscan-android/workflows/Lint/badge.svg)\n![Instrumentation Tests](https://github.com/getbouncer/cardscan-android/workflows/Instrumentation%20Tests/badge.svg)\n![Unit Tests](https://github.com/getbouncer/cardscan-android/workflows/Unit%20Tests/badge.svg)\n![Release](https://github.com/getbouncer/cardscan-android/workflows/Release/badge.svg)\n[![GitHub release](https://img.shields.io/github/release/getbouncer/cardscan-android.svg?maxAge=60)](https://github.com/getbouncer/cardscan-android/releases)\n\n![demo](docs/images/demo.gif)\n\n## Contents\n* [Requirements](#requirements)\n* [Demo](#demo)\n* [Integration](#integration)\n* [Customizing](#customizing)\n* [Developing](#developing)\n* [Authors](#authors)\n* [License](#license)\n\n## Requirements\n* Android API level 21 or higher\n* AndroidX compatibility\n* Kotlin coroutine compatibility\n\nNote: Your app does not have to be written in kotlin to integrate this library, but must be able to depend on kotlin functionality.\n\n## Demo\nThis repository contains a demonstration app for the CardScan product. To build and run the demo app, follow the instructions in the [demo documentation](https://docs.getbouncer.com/card-scan/android-integration-guide#demo).\n\n## Integration\nSee the [integration documentation](https://docs.getbouncer.com/card-scan/android-integration-guide) in the Bouncer Docs.\n\n### Provisioning an API key\nCardScan requires a valid API key to run. To provision an API key, visit the [Bouncer API console](https://api.getbouncer.com/console).\n\n### Name and expiration extraction support (BETA)\nTo test name and/or expiration extraction, please first provision an API key, then reach out to [bouncer-support@stripe.com](mailto:bouncer-support@stripe.com) with details about your use case and estimated volumes.\n\nBefore launching the CardScan flow, make sure to call the ```CardScanActivity.warmup()``` function with your API key and set ```initializeNameAndExpiryExtraction``` to ```true```\n\n```kotlin\nCardScanActivity.warmup(this, API_KEY, true)\n```\n\n## Customizing\nCardScan is built to be customized to fit your UI.\n\n### Basic modifications\nTo modify text, colors, or padding of the default UI, see the [customization](https://docs.getbouncer.com/card-scan/android-integration-guide/customization-guide) documentation.\n\n### Extensive modifications\nTo modify arrangement or UI functionality, CardScan can be used as a library for your custom implementation. See the [example single-activity demo app](demo/src/main/java/com/getbouncer/cardscan/demo/SingleActivityDemo.java).\n\n## Developing\nSee the [development docs](https://docs.getbouncer.com/card-scan/android-integration-guide/development-guide) for details on developing for CardScan.\n\n## Authors\nAdam Wushensky, Sam King, and Zain ul Abi Din\n\n## License\nThis library is available under the MIT license. See the [LICENSE](LICENSE) file for the full license text.\n"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    repositories {\n        mavenLocal()\n        mavenCentral()\n        google()\n        maven { url \"https://plugins.gradle.org/m2/\" }\n    }\n\n    dependencies {\n        classpath 'com.android.tools.build:gradle:4.2.2'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31\"\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\nplugins {\n    id 'org.jetbrains.kotlin.plugin.serialization' version \"1.5.31\"\n    id 'org.jetbrains.dokka' version '1.5.0'\n    id \"io.github.gradle-nexus.publish-plugin\" version \"1.1.0\"\n}\n\nallprojects {\n    apply plugin: 'checkstyle'\n\n    checkstyle {\n        toolVersion '8.29'\n    }\n\n    configurations {\n        javadocDeps\n        kotlinlint\n    }\n\n    dependencies {\n        kotlinlint(\"com.pinterest:ktlint:0.41.0\") {\n            attributes {\n                attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))\n            }\n        }\n    }\n\n    repositories {\n        mavenLocal()\n        mavenCentral()\n        google()\n        maven { url \"https://plugins.gradle.org/m2/\" }\n    }\n\n    task checkJavaStyle(type: Checkstyle) {\n        showViolations = true\n        configFile file(\"../settings/checkstyle.xml\")\n        source 'src/main/java'\n        include '**/*.java'\n        exclude '**/gen/**'\n        exclude '**/R.java'\n        exclude '**/BuildConfig.java'\n\n        // empty classpath\n        classpath = files()\n    }\n\n    task ktlint(type: JavaExec, group: \"verification\") {\n        description = \"Check Kotlin code style.\"\n        main = \"com.pinterest.ktlint.Main\"\n        classpath = configurations.kotlinlint\n        args \"src/**/*.kt\"\n        // to generate report in checkstyle format prepend following args:\n        // \"--reporter=plain\", \"--reporter=checkstyle,output=${buildDir}/ktlint.xml\"\n        // see https://github.com/pinterest/ktlint#usage for more\n    }\n\n    task ktlintFormat(type: JavaExec, group: \"formatting\") {\n        description = \"Fix Kotlin code style deviations.\"\n        main = \"com.pinterest.ktlint.Main\"\n        classpath = configurations.kotlinlint\n        args \"-F\", \"src/**/*.kt\"\n    }\n}\n\next {\n    publishGroupId = 'com.getbouncer'\n}\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    Properties p = new Properties()\n    new FileInputStream(secretPropsFile).withCloseable { is ->\n        p.load(is)\n    }\n    p.each { name, value ->\n        ext[name] = value\n    }\n} else {\n    ext[\"signing.keyId\"] = System.getenv('SIGNING_KEY_ID')\n    ext[\"signing.password\"] = System.getenv('SIGNING_PASSWORD')\n    ext[\"signing.secretKeyRingFile\"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')\n\n    ext[\"ossrhUsername\"] = System.getenv('OSSRH_USERNAME')\n    ext[\"ossrhPassword\"] = System.getenv('OSSRH_PASSWORD')\n\n    ext[\"sonatypeStagingProfileId\"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')\n}\n\nnexusPublishing {\n    repositories {\n        sonatype {\n            nexusUrl.set(uri(\"https://s01.oss.sonatype.org/service/local/\"))\n            snapshotRepositoryUrl.set(uri(\"https://s01.oss.sonatype.org/content/repositories/snapshots/\"))\n            packageGroup = publishGroupId\n            stagingProfileId = sonatypeStagingProfileId\n            username = ossrhUsername\n            password = ossrhPassword\n        }\n    }\n}\n"
  },
  {
    "path": "cardscan-demo/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "cardscan-demo/README.md",
    "content": "# Deprecation Notice\nHello from the Stripe (formerly Bouncer) team!\n\nWe're excited to provide an update on the state and future of the [Card Scan OCR](https://github.com/stripe/stripe-android/tree/master/stripecardscan) product! As we continue to build into Stripe's ecosystem, we'll be supporting the mission to continuously improve the end customer experience in many of Stripe's core checkout products.\n\nThis SDK has been [migrated to Stripe](https://github.com/stripe/stripe-android/tree/master/stripecardscan) and is now free for use under the MIT license!\n\nIf you are not currently a Stripe user, and interested in learning more about improving checkout experience through Stripe, please let us know and we can connect you with the team.\n\nIf you are not currently a Stripe user, and want to continue using the existing SDK, you can do so free of charge. Starting January 1, 2022, we will no longer be charging for use of the existing Bouncer Card Scan OCR SDK. For product support on [Android](https://github.com/stripe/stripe-android/issues) and [iOS](https://github.com/stripe/stripe-ios/issues). For billing support, please email [bouncer-support@stripe.com](mailto:bouncer-support@stripe.com).\n\nFor the new product, please visit the [stripe github repository](https://github.com/stripe/stripe-android/tree/master/stripecardscan).\n\n# Overview\nThis repository serves as a legacy, deprecated open source demonstration for the CardScan library. [CardScan](https://cardscan.io/) is a relatively small library that provides fast and accurate payment card scanning.\n\nCardScan is the foundation for CardVerify enterprise libraries, which validate the authenticity of payment cards as they are scanned.\n\n![demo](../docs/images/demo.gif)\n\n## Contents\n* [Requirements](#requirements)\n* [Demo](#demo)\n* [Integration](#integration)\n* [Customizing](#customizing)\n* [Developing](#developing)\n* [Authors](#authors)\n* [License](#license)\n\n## Requirements\n* Android API level 21 or higher\n* AndroidX compatibility\n* Kotlin coroutine compatibility\n\nNote: Your app does not have to be written in kotlin to integrate this library, but must be able to depend on kotlin functionality.\n\n## Demo\nThis repository contains a demonstration app for the CardScan product. To build and install this library follow the following steps:\n\n1. Clone the repository from github\n    ```bash\n    git clone --recursive https://github.com/getbouncer/cardscan-demo-android\n    ```\n    \n2. Build the library using gradle or [android studio](https://developer.android.com/studio).\n    a. Using android studio, open the directory `cardscan-demo-android`. Install the app on your device or an emulator by clicking the play button in the top right of android studio.\n    \n    ![build_android_studio](../docs/images/build_android_studio.png)\n    \n    b. Using gradle, build the demo app by executing the following command:\n    \n    ```bash\n    ./gradlew demo:assembleRelease\n    ```\n    This will create a release APK in the `cardscan-demo/build/outputs/apk` directory. Copy this file to your device and install it.\n\n## Integration\nSee the [integration documentation](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) in the Bouncer Docs.\n\n### Provisioning an API key\nCardScan requires a valid API key to run. To provision an API key, visit the [Bouncer API console](https://api.getbouncer.com/console).\n\n### Name and expiration extraction support (BETA)\nTo test name and/or expiration extraction, please first provision an API key, then reach out to [bouncer-support@stripe.com](mailto:bouncer-support@stripe.com) with details about your use case and estimated volumes.\n\nBefore launching the CardScan flow, make sure to call the ```CardScanActivity.warmup()``` function with your API key and set ```initializeNameAndExpiryExtraction``` to ```true```\n\n```kotlin\nCardScanActivity.warmup(this, API_KEY, true)\n```\n\n## Customizing\nCardScan is built to be customized to fit your UI.\n\n### Basic modifications\nTo modify text, colors, or padding of the default UI, see the [customization](https://docs.getbouncer.com/card-scan/android-integration-guide/android-customization-guide) documentation.\n\n### Extensive modifications\nTo modify arrangement or UI functionality, CardScan can be used as a library for your custom implementation. See the [example single-activity demo app](demo/src/main/java/com/getbouncer/cardscan/demo/SingleActivityDemo.java).\n\n## Developing\nSee the [development docs](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) for details on developing for CardScan.\n\n## Authors\nAdam Wushensky, Sam King, and Zain ul Abi Din\n\n## License\nThis library is available under the MIT license. See the [LICENSE](../LICENSE) file for the full license text.\n"
  },
  {
    "path": "cardscan-demo/build.gradle",
    "content": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\n\nandroid {\n    compileSdkVersion 30\n    buildToolsVersion '30.0.3'\n\n    defaultConfig {\n        applicationId \"com.getbouncer.cardscan.demo\"\n        minSdkVersion 21\n        targetSdkVersion 30\n        versionCode 1\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled true\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = JavaVersion.VERSION_1_8.toString()\n    }\n\n    lintOptions {\n        enable \"Interoperability\"\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n\n    implementation project(\":scan-camerax\")\n    implementation project(':scan-payment-full')\n    implementation project(\":cardscan-ui\")\n\n    implementation \"androidx.appcompat:appcompat:1.3.1\"\n    implementation \"androidx.core:core-ktx:1.6.0\"\n    implementation \"androidx.constraintlayout:constraintlayout:2.1.0\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1\"\n}\n\ndependencies {\n    testImplementation \"androidx.test:core:1.4.0\"\n    testImplementation \"androidx.test:runner:1.4.0\"\n    testImplementation \"junit:junit:4.13.2\"\n    testImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n\n    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'\n}\n\ndependencies {\n    androidTestImplementation \"androidx.test.ext:junit:1.1.3\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.4.0\"\n    androidTestImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n}\n"
  },
  {
    "path": "cardscan-demo/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": "cardscan-demo/src/androidTest/java/com/getbouncer/cardscan/demo/ExampleInstrumentedTest.kt",
    "content": "package com.getbouncer.cardscan.demo\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport org.junit.Test\nimport kotlin.test.assertEquals\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"com.getbouncer.cardscan.demo\", appContext.packageName)\n    }\n}\n"
  },
  {
    "path": "cardscan-demo/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.getbouncer.cardscan.demo\">\n\n    <uses-permission android:name=\"android.permission.CAMERA\" />\n    <uses-permission android:name=\"android.permission.FLASHLIGHT\"/>\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n\n    <uses-feature android:name=\"android.hardware.camera\" android:required=\"false\" />\n    <uses-feature android:name=\"android.hardware.camera.autofocus\" android:required=\"false\" />\n\n    <application\n        android:label=\"@string/app_name\"\n        android:allowBackup=\"false\"\n        android:supportsRtl=\"true\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:icon=\"@mipmap/ic_launcher\">\n\n        <activity\n            android:name=\".LaunchActivity\"\n            android:screenOrientation=\"fullSensor\"\n            android:theme=\"@style/AppTheme\">\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n\n        </activity>\n\n        <activity android:name=\".SingleActivityDemo\"\n            android:screenOrientation=\"nosensor\"\n            android:theme=\"@style/AppTheme\" />\n\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "cardscan-demo/src/main/java/com/getbouncer/cardscan/demo/LaunchActivity.java",
    "content": "package com.getbouncer.cardscan.demo;\n\nimport android.app.AlertDialog;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.view.WindowManager;\nimport android.widget.CheckBox;\nimport android.widget.TextView;\n\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport com.getbouncer.cardscan.ui.CardScanSheet;\nimport com.getbouncer.cardscan.ui.CardScanSheetResult;\nimport com.getbouncer.cardscan.ui.ScannedCard;\nimport com.getbouncer.scan.framework.Config;\nimport com.getbouncer.scan.framework.Scan;\nimport com.getbouncer.scan.ui.CancellationReason;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport kotlin.Unit;\n\npublic class LaunchActivity extends AppCompatActivity {\n\n    private static final String API_KEY = \"qOJ_fF-WLDMbG05iBq5wvwiTNTmM2qIn\";\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_launch);\n\n        final CardScanSheet sheet = CardScanSheet.create(this, API_KEY, this::handleScanResult);\n\n        // Because this activity displays card numbers, disallow screenshots.\n        getWindow().setFlags(\n            WindowManager.LayoutParams.FLAG_SECURE,\n            WindowManager.LayoutParams.FLAG_SECURE\n        );\n\n        ((CheckBox) findViewById(R.id.enableDebugCheckbox))\n                .setOnCheckedChangeListener((buttonView, isChecked) -> Config.setDebug(isChecked));\n\n        findViewById(R.id.scanCardButton).setOnClickListener(v -> {\n            final boolean enableNameExtraction =\n                ((CheckBox) findViewById(R.id.enableNameExtractionCheckbox)).isChecked();\n            final boolean enableExpiryExtraction =\n                ((CheckBox) findViewById(R.id.enableExpiryExtractionCheckbox)).isChecked();\n            final boolean enableEnterCardManually =\n                ((CheckBox) findViewById(R.id.enableEnterCardManuallyCheckbox)).isChecked();\n\n            sheet.present(\n                /* enableEnterCardManually */ enableEnterCardManually,\n                /* enableExpiryExtraction */ enableExpiryExtraction,\n                /* enableNameExtraction */ enableNameExtraction\n            );\n        });\n\n        if (Scan.INSTANCE.isDeviceArchitectureArm()) {\n            ((TextView) findViewById(R.id.deviceArchitectureText))\n                .setText(getString(\n                    R.string.deviceArchitecture,\n                    \"arm: \" + Scan.INSTANCE.getDeviceArchitecture()\n                ));\n        } else {\n            ((TextView) findViewById(R.id.deviceArchitectureText))\n                .setText(getString(\n                    R.string.deviceArchitecture,\n                    \"NOT arm\" + Scan.INSTANCE.getDeviceArchitecture()\n                ));\n        }\n\n        findViewById(R.id.singleActivityDemo).setOnClickListener(v ->\n                startActivity(new Intent(this, SingleActivityDemo.class))\n        );\n\n        CardScanSheet.prepareScan(this, API_KEY, true, () -> null);\n    }\n\n    private Unit handleScanResult(final CardScanSheetResult result) {\n        if (result instanceof CardScanSheetResult.Completed) {\n            cardScanned(((CardScanSheetResult.Completed) result).getScannedCard());\n        } else if (result instanceof CardScanSheetResult.Canceled) {\n            userCanceled(((CardScanSheetResult.Canceled) result).getReason());\n        } else if (result instanceof CardScanSheetResult.Failed) {\n            analyzerFailure(((CardScanSheetResult.Failed) result).getError());\n        }\n\n        return Unit.INSTANCE;\n    }\n\n    private void cardScanned(@NotNull final ScannedCard scanResult) {\n        AlertDialog.Builder builder = new AlertDialog.Builder(this);\n        StringBuilder message = new StringBuilder();\n        message.append(scanResult.getPan());\n        if (scanResult.getCardholderName() != null) {\n            message.append(\"\\nName: \");\n            message.append(scanResult.getCardholderName());\n        }\n        if (scanResult.getExpiryMonth() != null && scanResult.getExpiryYear() != null) {\n            message.append(\n                    String.format(\"\\nExpiry: %s/%s\",\n                    scanResult.getExpiryMonth(),\n                    scanResult.getExpiryYear())\n            );\n        }\n        if (scanResult.getErrorString() != null) {\n            message.append(\"\\nError: \");\n            message.append(scanResult.getErrorString());\n        }\n        builder.setMessage(message);\n        builder.show();\n    }\n\n    private void userCanceled(@NotNull final CancellationReason reason) {\n        AlertDialog.Builder builder = new AlertDialog.Builder(this);\n        if (reason instanceof CancellationReason.Back) {\n            builder.setMessage(R.string.user_pressed_back);\n        } else if (reason instanceof CancellationReason.Closed) {\n            builder.setMessage(R.string.scan_canceled);\n        } else if (reason instanceof CancellationReason.CameraPermissionDenied) {\n            builder.setMessage(R.string.permission_denied);\n        } else if (reason instanceof CancellationReason.UserCannotScan) {\n            builder.setMessage(R.string.enter_manually);\n        }\n        builder.show();\n    }\n\n    private void analyzerFailure(@NotNull final Throwable reason) {\n        AlertDialog.Builder builder = new AlertDialog.Builder(this);\n        builder.setMessage(reason.getMessage());\n        builder.show();\n    }\n}\n"
  },
  {
    "path": "cardscan-demo/src/main/java/com/getbouncer/cardscan/demo/SingleActivityDemo.java",
    "content": "package com.getbouncer.cardscan.demo;\n\nimport android.Manifest;\nimport android.annotation.SuppressLint;\nimport android.content.pm.PackageManager;\nimport android.graphics.Bitmap;\nimport android.graphics.PointF;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.util.Log;\nimport android.util.Size;\nimport android.view.View;\nimport android.widget.Button;\nimport android.widget.FrameLayout;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.core.app.ActivityCompat;\nimport androidx.core.content.ContextCompat;\n\nimport com.getbouncer.cardscan.ui.CardScanFlow;\nimport com.getbouncer.cardscan.ui.SavedFrame;\nimport com.getbouncer.cardscan.ui.analyzer.CompletionLoopAnalyzer;\nimport com.getbouncer.cardscan.ui.result.CompletionLoopListener;\nimport com.getbouncer.cardscan.ui.result.CompletionLoopResult;\nimport com.getbouncer.cardscan.ui.result.MainLoopAggregator;\nimport com.getbouncer.cardscan.ui.result.MainLoopState;\nimport com.getbouncer.scan.camera.CameraAdapter;\nimport com.getbouncer.scan.camera.CameraErrorListener;\nimport com.getbouncer.scan.camera.CameraPreviewImage;\nimport com.getbouncer.scan.camera.CameraSelectorKt;\nimport com.getbouncer.scan.framework.AggregateResultListener;\nimport com.getbouncer.scan.framework.AnalyzerLoopErrorListener;\nimport com.getbouncer.scan.framework.Config;\nimport com.getbouncer.scan.framework.Stats;\nimport com.getbouncer.scan.framework.api.BouncerApi;\nimport com.getbouncer.scan.framework.api.dto.ScanStatistics;\nimport com.getbouncer.scan.framework.interop.BlockingAggregateResultListener;\nimport com.getbouncer.scan.framework.util.AppDetails;\nimport com.getbouncer.scan.framework.util.Device;\nimport com.getbouncer.scan.payment.card.CardExpiryKt;\nimport com.getbouncer.scan.payment.card.PanFormatterKt;\nimport com.getbouncer.scan.payment.card.PaymentCardUtils;\nimport com.getbouncer.scan.ui.ViewFinderBackground;\nimport com.getbouncer.scan.ui.util.ViewExtensionsKt;\n\nimport java.util.Locale;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport kotlin.Unit;\nimport kotlin.coroutines.CoroutineContext;\nimport kotlinx.coroutines.CoroutineScope;\nimport kotlinx.coroutines.Dispatchers;\n\npublic class SingleActivityDemo extends AppCompatActivity implements CameraErrorListener,\n        AnalyzerLoopErrorListener, CoroutineScope {\n\n    private enum State {\n        NOT_FOUND,\n        FOUND,\n        CORRECT\n    }\n\n    private static final int PERMISSION_REQUEST_CODE = 1200;\n    private static final Size MINIMUM_RESOLUTION = new Size(1280, 720);\n\n    private Button scanCardButton;\n    private View scanView;\n\n    private FrameLayout cameraPreview;\n\n    private FrameLayout viewFinderWindow;\n    private ViewFinderBackground viewFinderBackground;\n    private ImageView viewFinderBorder;\n    private View processingOverlay;\n\n    private ImageView flashButtonView;\n\n    private TextView cardPanTextView;\n\n    private CameraAdapter<CameraPreviewImage<Bitmap>> cameraAdapter;\n\n    private CardScanFlow cardScanFlow;\n\n    private State scanState = State.NOT_FOUND;\n\n    private String pan = null;\n\n    /**\n     * CardScan uses kotlin coroutines to run multiple analyzers in parallel for maximum image\n     * throughput. This coroutine context binds the coroutines to this activity, so that if this\n     * activity is terminated, all coroutines are terminated and there is no work leak.\n     *\n     * Additionally, this specifies which threads the coroutines will run on. Normally, the default\n     * dispatchers should be used so that coroutines run on threads bound by the number of CPU\n     * cores.\n     */\n    @NotNull\n    @Override\n    public CoroutineContext getCoroutineContext() {\n        return Dispatchers.getDefault();\n    }\n\n    @Override\n    @SuppressLint(\"ClickableViewAccessibility\")\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_single_demo);\n\n        scanCardButton = findViewById(R.id.scanCardButton);\n        scanView = findViewById(R.id.scanView);\n\n        scanCardButton.setOnClickListener(v -> {\n            scanCardButton.setVisibility(View.GONE);\n            scanView.setVisibility(View.VISIBLE);\n\n            if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) !=\n                    PackageManager.PERMISSION_GRANTED) {\n                requestCameraPermission();\n            } else {\n                startScan();\n            }\n        });\n\n        cameraPreview = findViewById(R.id.cameraPreviewHolder);\n        viewFinderWindow = findViewById(R.id.viewFinderWindow);\n        viewFinderBackground = findViewById(R.id.viewFinderBackground);\n        viewFinderBorder = findViewById(R.id.viewFinderBorder);\n        processingOverlay = findViewById(R.id.processing_overlay);\n\n        flashButtonView = findViewById(R.id.flashButtonView);\n        ImageView closeButtonView = findViewById(R.id.closeButtonView);\n\n        cardPanTextView = findViewById(R.id.cardPanTextView);\n\n        closeButtonView.setOnClickListener(v -> userCancelScan());\n        flashButtonView.setOnClickListener(v -> setFlashlightState(!cameraAdapter.isTorchOn()));\n\n        // Allow the user to set the focus of the camera by tapping on the view finder.\n        viewFinderWindow.setOnTouchListener((v, event) -> {\n            cameraAdapter.setFocus(new PointF(\n                event.getX() + viewFinderWindow.getLeft(), \n                event.getY() + viewFinderWindow.getTop())\n            );\n            return true;\n        });\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        if (cardScanFlow != null) {\n            cardScanFlow.cancelFlow();\n        }\n    }\n\n    @Override\n    protected void onPause() {\n        super.onPause();\n        setFlashlightState(false);\n    }\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n        setStateNotFound();\n    }\n\n    /**\n     * Request permission to use the camera.\n     */\n    private void requestCameraPermission() {\n        ActivityCompat.requestPermissions(\n            this,\n            new String[] { Manifest.permission.CAMERA },\n            PERMISSION_REQUEST_CODE\n        );\n    }\n\n    /**\n     * Handle permission status changes. If the camera permission has been granted, start it. If\n     * not, show a dialog.\n     */\n    @Override\n    public void onRequestPermissionsResult(\n        int requestCode,\n        @NotNull String[] permissions,\n        @NotNull int[] grantResults\n    ) {\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults);\n\n        if (requestCode == PERMISSION_REQUEST_CODE && grantResults.length > 0) {\n            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {\n                startScan();\n            } else {\n                showPermissionDeniedDialog();\n            }\n        }\n    }\n\n    /**\n     * Show an explanation dialog for why we are requesting camera permissions.\n     */\n    private void showPermissionDeniedDialog() {\n        final AlertDialog.Builder builder = new AlertDialog.Builder(this);\n        builder.setMessage(R.string.bouncer_camera_permission_denied_message)\n            .setPositiveButton(\n                R.string.bouncer_camera_permission_denied_ok,\n                (dialog, which) -> requestCameraPermission()\n            )\n            .setNegativeButton(\n                R.string.bouncer_camera_permission_denied_cancel,\n                (dialog, which) -> startScan()\n            )\n            .show();\n    }\n\n    /**\n     * Start the scanning flow.\n     */\n    private void startScan() {\n        // ensure the cameraPreview view has rendered.\n        cameraPreview.post(() -> {\n            // Track scan statistics for health check\n            Stats.INSTANCE.startScan();\n\n            // Tell the background where to draw a hole for the viewfinder window\n            viewFinderBackground.setViewFinderRect(ViewExtensionsKt.asRect(viewFinderWindow));\n\n            // Create a camera adapter and bind it to this activity.\n            cameraAdapter = CameraSelectorKt.getCameraAdapter(\n                this,\n                cameraPreview,\n                MINIMUM_RESOLUTION,\n                this\n            );\n            cameraAdapter.bindToLifecycle(this);\n            cameraAdapter.withFlashSupport(supported -> {\n                flashButtonView.setVisibility(supported ? View.VISIBLE : View.INVISIBLE);\n                return Unit.INSTANCE;\n            });\n\n            // Create and start a CardScanFlow which will handle the business logic of the scan\n            cardScanFlow = new CardScanFlow(\n                true,\n                true,\n                aggregateResultListener,\n                this\n            );\n            cardScanFlow.startFlow(\n                this,\n                cameraAdapter.getImageStream(),\n                ViewExtensionsKt.asRect(viewFinderWindow),\n                this,\n                this\n            );\n        });\n    }\n\n    /**\n     * Turn the flashlight on or off.\n     */\n    private void setFlashlightState(boolean on) {\n        if (cameraAdapter != null) {\n            cameraAdapter.setTorchState(on);\n\n            if (cameraAdapter.isTorchOn()) {\n                flashButtonView.setImageResource(R.drawable.bouncer_flash_on_dark);\n            } else {\n                flashButtonView.setImageResource(R.drawable.bouncer_flash_off_dark);\n            }\n        }\n    }\n\n    /**\n     * Cancel scanning due to analyzer failure\n     */\n    private void analyzerFailureCancelScan(@Nullable final Throwable cause) {\n        Log.e(Config.getLogTag(), \"Canceling scan due to analyzer error\", cause);\n        new AlertDialog.Builder(this)\n            .setMessage(\"Analyzer failure\")\n            .show();\n        closeScanner();\n    }\n\n    /**\n     * Cancel scanning due to a camera error.\n     */\n    private void cameraErrorCancelScan(@Nullable final Throwable cause) {\n        Log.e(Config.getLogTag(), \"Canceling scan due to camera error\", cause);\n        new AlertDialog.Builder(this)\n            .setMessage(\"Camera error\")\n            .show();\n        closeScanner();\n    }\n\n    /**\n     * The scan has been cancelled by the user.\n     */\n    private void userCancelScan() {\n        new AlertDialog.Builder(this)\n            .setMessage(\"Scan Canceled by user\")\n            .show();\n        closeScanner();\n    }\n\n    /**\n     * Show the completed scan results\n     */\n    private void completeScan(\n        @Nullable String expiryMonth,\n        @Nullable String expiryYear,\n        @Nullable String cardNumber,\n        @Nullable String issuer,\n        @Nullable String name,\n        @Nullable String error\n    ) {\n        new AlertDialog.Builder(this)\n            .setMessage(String.format(\n                Locale.getDefault(),\n                \"%s\\n%s\\n%s/%s\\n%s\\n%s\",\n                cardNumber,\n                issuer,\n                expiryMonth,\n                expiryYear,\n                name,\n                error\n            ))\n            .show();\n        closeScanner();\n    }\n\n    /**\n     * Close the scanner.\n     */\n    private void closeScanner() {\n        setFlashlightState(false);\n        scanCardButton.setVisibility(View.VISIBLE);\n        scanView.setVisibility(View.GONE);\n        setStateNotFound();\n        cameraAdapter.unbindFromLifecycle(this);\n        if (cardScanFlow != null) {\n            cardScanFlow.cancelFlow();\n        }\n        BouncerApi.uploadScanStats(\n            this,\n            Stats.INSTANCE.getInstanceId(),\n            Stats.INSTANCE.getScanId(),\n            Device.fromContext(this),\n            AppDetails.fromContext(this),\n            ScanStatistics.fromStats()\n        );\n    }\n\n    @Override\n    public void onCameraOpenError(@Nullable Throwable cause) {\n        cameraErrorCancelScan(cause);\n    }\n\n    @Override\n    public void onCameraAccessError(@Nullable Throwable cause) {\n        cameraErrorCancelScan(cause);\n    }\n\n    @Override\n    public void onCameraUnsupportedError(@Nullable Throwable cause) {\n        cameraErrorCancelScan(cause);\n    }\n\n    @Override\n    public boolean onAnalyzerFailure(@NotNull Throwable t) {\n        analyzerFailureCancelScan(t);\n        return true;\n    }\n\n    @Override\n    public boolean onResultFailure(@NotNull Throwable t) {\n        analyzerFailureCancelScan(t);\n        return true;\n    }\n\n    private final CompletionLoopListener completionLoopListener = new CompletionLoopListener() {\n        @Override\n        public void onCompletionLoopFrameProcessed(\n                @NotNull CompletionLoopAnalyzer.Prediction result,\n                @NotNull SavedFrame frame\n        ) {\n            // display debug information if so desired\n        }\n\n        @Override\n        public void onCompletionLoopDone(@NotNull CompletionLoopResult result) {\n            @Nullable final String expiryMonth;\n            @Nullable final String expiryYear;\n            if (result.getExpiryMonth() != null &&\n                result.getExpiryYear() != null &&\n                CardExpiryKt.isValidExpiry(\n                    null,\n                    result.getExpiryMonth(),\n                    result.getExpiryYear()\n                )\n            ) {\n                expiryMonth = result.getExpiryMonth();\n                expiryYear = result.getExpiryYear();\n            } else {\n                expiryMonth = null;\n                expiryYear = null;\n            }\n\n            new Handler(getMainLooper()).post(() -> {\n                // Only show the expiry dates that are not expired\n                completeScan(\n                    expiryMonth,\n                    expiryYear,\n                    SingleActivityDemo.this.pan,\n                    PaymentCardUtils.getCardIssuer(SingleActivityDemo.this.pan).getDisplayName(),\n                    result.getName(),\n                    result.getErrorString()\n                );\n            });\n        }\n    };\n\n    private final AggregateResultListener<\n            MainLoopAggregator.InterimResult,\n            MainLoopAggregator.FinalResult> aggregateResultListener =\n            new BlockingAggregateResultListener<\n                MainLoopAggregator.InterimResult,\n                MainLoopAggregator.FinalResult>() {\n\n        /**\n         * An interim result has been received from the scan, the scan is still running. Update your\n         * UI as necessary here to display the progress of the scan.\n         */\n        @Override\n        public void onInterimResultBlocking(MainLoopAggregator.InterimResult interimResult) {\n            new Handler(getMainLooper()).post(() -> {\n                final MainLoopState mainLoopState = interimResult.getState();\n\n                if (mainLoopState instanceof MainLoopState.Initial) {\n                    // In initial state, show no card found\n                    setStateNotFound();\n\n                } else if (mainLoopState instanceof MainLoopState.PanFound) {\n                    // If OCR is running and a valid card number is visible, display it\n                    final MainLoopState.PanFound state = (MainLoopState.PanFound) mainLoopState;\n                    final String pan = state.getMostLikelyPan();\n                    if (pan != null) {\n                        cardPanTextView.setText(PanFormatterKt.formatPan(pan));\n                        ViewExtensionsKt.show(cardPanTextView);\n                    }\n                    setStateFound();\n\n                } else if (mainLoopState instanceof MainLoopState.CardSatisfied) {\n                    // If OCR is running and a valid card number is visible, display it\n                    final MainLoopState.CardSatisfied state =\n                            (MainLoopState.CardSatisfied) mainLoopState;\n                    final String pan = state.getMostLikelyPan();\n                    if (pan != null) {\n                        cardPanTextView.setText(PanFormatterKt.formatPan(pan));\n                        ViewExtensionsKt.show(cardPanTextView);\n                    }\n\n                    setStateFound();\n\n                } else if (mainLoopState instanceof MainLoopState.PanSatisfied) {\n                    // If OCR is running and a valid card number is visible, display it\n                    final MainLoopState.PanSatisfied state =\n                            (MainLoopState.PanSatisfied) mainLoopState;\n                    final String pan = state.getPan();\n                    if (pan != null) {\n                        cardPanTextView.setText(PanFormatterKt.formatPan(pan));\n                        ViewExtensionsKt.show(cardPanTextView);\n                    }\n\n                    setStateFound();\n\n                } else if (mainLoopState instanceof MainLoopState.Finished) {\n                    // Once the main loop has finished, the camera can stop\n                    cameraAdapter.unbindFromLifecycle(SingleActivityDemo.this);\n                    setStateCorrect();\n\n                }\n            });\n        }\n\n        /**\n         * The scan has completed and the final result is available. Close the scanner and make use\n         * of the final result.\n         */\n        @Override\n        public void onResultBlocking(MainLoopAggregator.FinalResult result) {\n            SingleActivityDemo.this.pan = result.getPan();\n            cardScanFlow.launchCompletionLoop(\n                SingleActivityDemo.this,\n                completionLoopListener,\n                cardScanFlow.selectCompletionLoopFrames(\n                    result.getAverageFrameRate(),\n                    result.getSavedFrames()\n                ),\n                result.getAverageFrameRate().compareTo(Config.getSlowDeviceFrameRate()) > 0,\n                SingleActivityDemo.this\n            );\n        }\n\n        /**\n         * The scan was reset (usually because the activity was backgrounded). Reset the UI.\n         */\n        @Override\n        public void onResetBlocking() {\n            new Handler(getMainLooper()).post(() -> setStateNotFound());\n        }\n    };\n\n    /**\n     * Display a blue border tracing the outline of the card to indicate that the card is identified\n     * and scanning is running.\n     */\n    private void setStateFound() {\n        if (scanState == State.FOUND) return;\n        ViewExtensionsKt.startAnimation(viewFinderBorder,\n            R.drawable.bouncer_card_border_found_long);\n        ViewExtensionsKt.hide(processingOverlay);\n        scanState = State.FOUND;\n    }\n\n    /**\n     * Return the view to its initial state, where no card has been detected.\n     */\n    private void setStateNotFound() {\n        if (scanState == State.NOT_FOUND) return;\n        ViewExtensionsKt.startAnimation(viewFinderBorder, R.drawable.bouncer_card_border_not_found);\n        ViewExtensionsKt.hide(cardPanTextView);\n        ViewExtensionsKt.hide(processingOverlay);\n        scanState = State.NOT_FOUND;\n    }\n\n    /**\n     * Flash the border around the card green to indicate that scanning was successful.\n     */\n    private void setStateCorrect() {\n        if (scanState == State.CORRECT) return;\n        ViewExtensionsKt.startAnimation(viewFinderBorder, R.drawable.bouncer_card_border_correct);\n        ViewExtensionsKt.show(processingOverlay);\n        scanState = State.CORRECT;\n    }\n}\n"
  },
  {
    "path": "cardscan-demo/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillColor=\"#008577\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "cardscan-demo/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"78.5885\"\n                android:endY=\"90.9159\"\n                android:startX=\"48.7653\"\n                android:startY=\"61.0927\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        android:pathData=\"M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\" />\n</vector>\n"
  },
  {
    "path": "cardscan-demo/src/main/res/layout/activity_launch.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView\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=\"match_parent\" >\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        tools:context=\".LaunchActivity\">\n\n        <LinearLayout\n            android:layout_marginHorizontal=\"16dp\"\n            android:layout_marginVertical=\"4dp\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/enableDebug\" />\n\n            <CheckBox\n                android:id=\"@+id/enableDebugCheckbox\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_marginHorizontal=\"16dp\"\n            android:layout_marginVertical=\"4dp\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/enableNameExtraction\" />\n\n            <CheckBox\n                android:id=\"@+id/enableNameExtractionCheckbox\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_marginHorizontal=\"16dp\"\n            android:layout_marginVertical=\"4dp\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/enableExpiryExtraction\" />\n\n            <CheckBox\n                android:id=\"@+id/enableExpiryExtractionCheckbox\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_marginHorizontal=\"16dp\"\n            android:layout_marginVertical=\"4dp\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/enableEnterCardManually\" />\n\n            <CheckBox\n                android:id=\"@+id/enableEnterCardManuallyCheckbox\"\n                android:checked=\"true\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n        </LinearLayout>\n\n        <Button\n            android:id=\"@+id/scanCardButton\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginHorizontal=\"16dp\"\n            android:layout_marginVertical=\"4dp\"\n            android:text=\"@string/scan_card\" />\n\n        <Button\n            android:id=\"@+id/singleActivityDemo\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginHorizontal=\"16dp\"\n            android:layout_marginVertical=\"4dp\"\n            android:text=\"@string/single_activity_demo\" />\n\n        <TextView\n            android:id=\"@+id/deviceArchitectureText\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginHorizontal=\"16dp\"\n            android:layout_marginVertical=\"4dp\"\n            android:text=\"@string/deviceArchitecture\" />\n\n    </LinearLayout>\n\n</ScrollView>\n"
  },
  {
    "path": "cardscan-demo/src/main/res/layout/activity_single_demo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge\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    tools:context=\".SingleActivityDemo\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\" >\n\n    <Button\n        android:id=\"@+id/scanCardButton\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:text=\"@string/scan_card\" />\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/scanView\"\n        android:visibility=\"invisible\"\n        android:background=\"@color/bouncerNotFoundBackground\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" >\n\n        <FrameLayout\n            android:id=\"@+id/cameraPreviewHolder\"\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_toTopOf=\"parent\" />\n\n        <com.getbouncer.scan.ui.ViewFinderBackground\n            android:id=\"@+id/viewFinderBackground\"\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_toTopOf=\"parent\"\n            app:backgroundColor=\"@color/bouncerNotFoundBackground\" />\n\n        <FrameLayout\n            android:id=\"@+id/viewFinderWindow\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:layout_marginStart=\"24dp\"\n            android:layout_marginEnd=\"24dp\"\n            android:layout_marginTop=\"24dp\"\n            android:layout_marginBottom=\"24dp\"\n            android:background=\"@drawable/bouncer_card_background_not_found\"\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            app:layout_constraintVertical_bias=\"@dimen/bouncerViewFinderVerticalBias\"\n            app:layout_constraintHorizontal_bias=\"@dimen/bouncerViewFinderHorizontalBias\"\n            app:layout_constraintDimensionRatio=\"H,200:126\">\n\n            <ImageView\n                android:id=\"@+id/viewFinderBorder\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:src=\"@drawable/bouncer_card_border_not_found\"\n                android:background=\"@drawable/bouncer_card_border_not_found\"\n                android:contentDescription=\"@string/bouncer_card_view_finder_description\" />\n\n        </FrameLayout>\n\n        <TextView\n            android:id=\"@+id/instructionsTextView\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/bouncer_card_scan_instructions\"\n            android:textColor=\"@color/bouncerInstructionsColorDark\"\n            android:textSize=\"@dimen/bouncerInstructionsTextSize\"\n            android:gravity=\"center\"\n            android:layout_margin=\"@dimen/bouncerInstructionsMargin\"\n            app:layout_constraintBottom_toTopOf=\"@id/viewFinderWindow\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\" />\n\n        <ImageView\n            android:id=\"@+id/securityIconView\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"0dp\"\n            android:contentDescription=\"@string/bouncer_security_description\"\n            android:src=\"@drawable/bouncer_lock_dark\"\n            android:layout_marginEnd=\"@dimen/bouncerSecurityIconMargin\"\n            app:layout_constraintHorizontal_chainStyle=\"packed\"\n            app:layout_constraintTop_toTopOf=\"@id/securityTextView\"\n            app:layout_constraintBottom_toBottomOf=\"@id/securityTextView\"\n            app:layout_constraintStart_toStartOf=\"@id/viewFinderWindow\"\n            app:layout_constraintEnd_toStartOf=\"@id/securityTextView\" />\n\n        <TextView\n            android:id=\"@+id/securityTextView\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/bouncer_card_scan_security\"\n            android:textSize=\"@dimen/bouncerSecurityTextSize\"\n            android:textColor=\"@color/bouncerSecurityColorDark\"\n            android:layout_margin=\"@dimen/bouncerSecurityMargin\"\n            app:layout_constraintTop_toBottomOf=\"@id/viewFinderWindow\"\n            app:layout_constraintStart_toEndOf=\"@id/securityIconView\"\n            app:layout_constraintEnd_toEndOf=\"@id/viewFinderWindow\" />\n\n        <LinearLayout\n            android:id=\"@+id/cardDetailsView\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            android:layout_gravity=\"center_horizontal\"\n            android:layout_marginStart=\"@dimen/bouncerCardDetailsMargin\"\n            android:layout_marginEnd=\"@dimen/bouncerCardDetailsMargin\"\n            app:layout_constraintTop_toTopOf=\"@id/viewFinderWindow\"\n            app:layout_constraintBottom_toBottomOf=\"@id/viewFinderWindow\"\n            app:layout_constraintStart_toStartOf=\"@id/viewFinderWindow\"\n            app:layout_constraintEnd_toEndOf=\"@id/viewFinderWindow\" >\n\n            <TextView\n                android:id=\"@+id/cardPanTextView\"\n                android:textColor=\"@color/bouncerCardPanColor\"\n                android:textSize=\"@dimen/bouncerPanTextSize\"\n                android:shadowColor=\"@color/bouncerCardPanOutlineColor\"\n                android:shadowDx=\"0\"\n                android:shadowDy=\"0\"\n                android:shadowRadius=\"2.5\"\n                android:gravity=\"center\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:visibility=\"invisible\" />\n\n        </LinearLayout>\n\n        <ImageView\n            android:id=\"@+id/closeButtonView\"\n            android:src=\"@drawable/bouncer_close_button_dark\"\n            android:contentDescription=\"@string/bouncer_close_button_description\"\n            android:layout_height=\"wrap_content\"\n            android:layout_width=\"wrap_content\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n\n        <ImageView\n            android:id=\"@+id/flashButtonView\"\n            android:src=\"@drawable/bouncer_flash_off_dark\"\n            android:contentDescription=\"@string/bouncer_close_button_description\"\n            android:layout_height=\"wrap_content\"\n            android:layout_width=\"wrap_content\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n\n        <ImageView\n            android:id=\"@+id/cardscanLogo\"\n            android:src=\"@drawable/bouncer_logo_dark_background\"\n            android:contentDescription=\"@string/bouncer_cardscan_logo\"\n            android:layout_width=\"100dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/bouncerButtonMargin\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/processing_overlay\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            android:visibility=\"gone\"\n            android:background=\"@color/bouncerProcessingBackground\" >\n\n            <ProgressBar\n                android:id=\"@+id/processing_spinner\"\n                android:layout_width=\"wrap_content\"\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                app:layout_constraintBottom_toBottomOf=\"parent\" />\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/bouncer_processing_card\"\n                android:textColor=\"@color/bouncerProcessingText\"\n                android:textSize=\"@dimen/bouncerPanTextSize\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintEnd_toEndOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/processing_spinner\" />\n\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</merge>\n"
  },
  {
    "path": "cardscan-demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "cardscan-demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "cardscan-demo/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#008577</color>\n    <color name=\"colorPrimaryDark\">#00574B</color>\n    <color name=\"colorAccent\">#D81B60</color>\n</resources>\n"
  },
  {
    "path": "cardscan-demo/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">CardScan Demo</string>\n\n    <string name=\"scan_card\">Scan Card</string>\n\n    <string name=\"user_pressed_back\">User pressed back</string>\n    <string name=\"scan_canceled\">Scan canceled</string>\n    <string name=\"enter_manually\">Enter card manually</string>\n    <string name=\"permission_denied\">Camera permission denied</string>\n    <string name=\"single_activity_demo\">Single Activity Demo</string>\n\n    <string name=\"enableDebug\">Enable Debug</string>\n    <string name=\"enableNameExtraction\">Enable name extraction</string>\n    <string name=\"enableExpiryExtraction\">Enable expiry extraction</string>\n    <string name=\"enableEnterCardManually\">Enable manual entry</string>\n\n    <string name=\"deviceArchitecture\">Device architecture is %1$s</string>\n</resources>\n"
  },
  {
    "path": "cardscan-demo/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "cardscan-demo/src/test/java/com/getbouncer/cardscan/demo/ExampleUnitTest.kt",
    "content": "package com.getbouncer.cardscan.demo\n\nimport org.junit.Test\nimport kotlin.test.assertEquals\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}\n"
  },
  {
    "path": "cardscan-ui/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "cardscan-ui/README.md",
    "content": "# Deprecation Notice\nHello from the Stripe (formerly Bouncer) team!\n\nWe're excited to provide an update on the state and future of the [Card Scan OCR](https://github.com/stripe/stripe-android/tree/master/stripecardscan) product! As we continue to build into Stripe's ecosystem, we'll be supporting the mission to continuously improve the end customer experience in many of Stripe's core checkout products.\n\nThis SDK has been [migrated to Stripe](https://github.com/stripe/stripe-android/tree/master/stripecardscan) and is now free for use under the MIT license!\n\nIf you are not currently a Stripe user, and interested in learning more about improving checkout experience through Stripe, please let us know and we can connect you with the team.\n\nIf you are not currently a Stripe user, and want to continue using the existing SDK, you can do so free of charge. Starting January 1, 2022, we will no longer be charging for use of the existing Bouncer Card Scan OCR SDK. For product support on [Android](https://github.com/stripe/stripe-android/issues) and [iOS](https://github.com/stripe/stripe-ios/issues). For billing support, please email [bouncer-support@stripe.com](mailto:bouncer-support@stripe.com).\n\nFor the new product, please visit the [stripe github repository](https://github.com/stripe/stripe-android/tree/master/stripecardscan).\n\n# Overview\nThis repository provides the legacy, deprecated open source user interfaces for the CardScan product. [CardScan](https://cardscan.io/) is a relatively small library that provides fast and accurate payment card scanning.\n\nThis library is the foundation for CardScan and CardVerify enterprise libraries, which validate the authenticity of payment cards as they are scanned.\n\n![demo](../docs/images/demo.gif)\n\n## Contents\n* [Requirements](#requirements)\n* [Demo](#demo)\n* [Integration](#integration)\n* [Customizing](#customizing)\n* [Developing](#developing)\n* [Authors](#authors)\n* [License](#license)\n\n## Requirements\n* Android API level 21 or higher\n* AndroidX compatibility\n* Kotlin coroutine compatibility\n\nNote: Your app does not have to be written in kotlin to integrate this library, but must be able to depend on kotlin functionality.\n\n## Demo\nAn app demonstrating the basic capabilities of this library is available in [github](https://github.com/getbouncer/cardscan-demo-android).\n\n## Integration\nSee the [integration documentation](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) in the Bouncer Docs.\n\n### Provisioning an API key\nCardScan requires a valid API key to run. To provision an API key, visit the [Bouncer API console](https://api.getbouncer.com/console).\n\n### Name and expiration extraction support (BETA)\nTo test name and/or expiration extraction, please first provision an API key, then reach out to [bouncer-support@stripe.com](mailto:bouncer-support@stripe.com) with details about your use case and estimated volumes.\n\nBefore launching the CardScan flow, make sure to call the ```CardScanActivity.warmup()``` function with your API key and set ```initializeNameAndExpiryExtraction``` to ```true```\n\n```kotlin\nCardScanActivity.warmup(this, API_KEY, true)\n```\n\n## Customizing\nCardScan is built to be customized to fit your UI.\n\n### Basic modifications\nTo modify text, colors, or padding of the default UI, see the [customization](https://docs.getbouncer.com/card-scan/android-integration-guide/android-customization-guide) documentation.\n\n### Extensive modifications\nTo modify arrangement or UI functionality, CardScan can be used as a library for your custom implementation. See the [example single-activity demo app](https://github.com/getbouncer/cardscan-demo-android/blob/master/demo/src/main/java/com/getbouncer/cardscan/demo/SingleActivityDemo.java).\n\n## Developing\nSee the [development docs](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) for details on developing for CardScan.\n\n## Authors\nAdam Wushensky, Sam King, and Zain ul Abi Din\n\n## License\nThis library is available under the MIT license. See the [LICENSE](../LICENSE) file for the full license text.\n"
  },
  {
    "path": "cardscan-ui/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-parcelize'\n\nandroid {\n    compileSdkVersion 30\n    buildToolsVersion '30.0.3'\n\n    defaultConfig {\n        minSdkVersion 21\n        targetSdkVersion 30\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles 'consumer-rules.pro'\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    testOptions {\n        unitTests.includeAndroidResources = true\n    }\n\n    lintOptions {\n        enable \"Interoperability\"\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = JavaVersion.VERSION_1_8.toString()\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    api project(\":scan-framework\")\n    api project(':scan-camera')\n    api project(\":scan-payment\")\n    api project(\":scan-ui\")\n\n    implementation \"androidx.appcompat:appcompat:[1.3.0,1.3.1]\"\n    implementation \"androidx.core:core-ktx:[1.3.1,1.6.0]\"\n    implementation 'androidx.constraintlayout:constraintlayout:[2.0.4,2.1.0]'\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-core:[1.4.0,1.5.1]\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-serialization-json:[1.1.0,1.2.2]\"\n}\n\ndependencies {\n    testImplementation \"androidx.test:core:1.4.0\"\n    testImplementation \"androidx.test:runner:1.4.0\"\n    testImplementation \"junit:junit:4.13.2\"\n    testImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n    testImplementation \"org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1\"\n}\n\ndependencies {\n    androidTestImplementation \"androidx.test.ext:junit:1.1.3\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.4.0\"\n    androidTestImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n}\n\napply from: 'deploy.gradle'\n"
  },
  {
    "path": "cardscan-ui/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "cardscan-ui/deploy.gradle",
    "content": "apply plugin: 'maven-publish'\napply plugin: 'org.jetbrains.dokka'\napply plugin: 'signing'\n\ntask androidSourcesJar(type: Jar) {\n    archiveClassifier.set('sources')\n    if (project.plugins.findPlugin(\"com.android.library\")) {\n        // Android library\n        from android.sourceSets.main.java.srcDirs\n        from android.sourceSets.main.kotlin.srcDirs\n    } else {\n        // Pure kotlin library\n        from sourceSets.main.java.srcDirs\n        from sourceSets.main.kotlin.srcDirs\n    }\n}\n\ntasks.withType(dokkaHtmlPartial.getClass()).configureEach {\n    pluginsMapConfiguration.set(\n            [\"org.jetbrains.dokka.base.DokkaBase\": \"\"\"{ \"separateInheritedMembers\": true}\"\"\"]\n    )\n}\n\ntask javadocJar(type: Jar, dependsOn: dokkaJavadoc) {\n    archiveClassifier.set('javadoc')\n    from dokkaJavadoc.outputDirectory\n}\n\nartifacts {\n    archives androidSourcesJar\n    archives javadocJar\n}\n\next[\"signing.keyId\"] = ''\next[\"signing.password\"] = ''\next[\"signing.secretKeyRingFile\"] = ''\n\next[\"ossrhUsername\"] = ''\next[\"ossrhPassword\"] = ''\next[\"sonatypeStagingProfileId\"] = ''\n\next {\n    libraryDescription = 'This library provides the user interface for scanning'\n\n    siteUrl = 'https://getbouncer.com'\n\n    scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'\n    scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'\n    scmUrl = 'https://github.com/getbouncer/cardscan-android'\n\n    licenseName = 'bouncer-free-1'\n    licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'\n\n    developerId = 'getbouncer'\n    developerName = 'Bouncer Technologies'\n    developerEmail = 'bouncer-support@stripe.com'\n\n    publishGroupId = 'com.getbouncer'\n    publishArtifactId = 'cardscan-ui'\n    publishVersion = version\n}\n\ngroup = publishGroupId\nversion = publishVersion\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    Properties p = new Properties()\n    new FileInputStream(secretPropsFile).withCloseable { is ->\n        p.load(is)\n    }\n    p.each { name, value ->\n        ext[name] = value\n    }\n} else {\n    ext[\"signing.keyId\"] = System.getenv('SIGNING_KEY_ID')\n    ext[\"signing.password\"] = System.getenv('SIGNING_PASSWORD')\n    ext[\"signing.secretKeyRingFile\"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')\n\n    ext[\"ossrhUsername\"] = System.getenv('OSSRH_USERNAME')\n    ext[\"ossrhPassword\"] = System.getenv('OSSRH_PASSWORD')\n\n    ext[\"sonatypeStagingProfileId\"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')\n}\n\npublishing {\n    publications {\n        release(MavenPublication) {\n            groupId publishGroupId\n            artifactId publishArtifactId\n            version publishVersion\n\n            // Two artifacts, the `aar` (or `jar`) and the sources\n            if (project.plugins.findPlugin(\"com.android.library\")) {\n                artifact(\"$buildDir/outputs/aar/${project.getName()}-release.aar\")\n            } else {\n                artifact(\"$buildDir/libs/${project.getName()}-${version}.jar\")\n            }\n            artifact androidSourcesJar\n\n            pom {\n                name = publishArtifactId\n                description = libraryDescription\n                url = siteUrl\n                licenses {\n                    license {\n                        name = licenseName\n                        url = licenseUrl\n                    }\n                }\n                developers {\n                    developer {\n                        id = developerId\n                        name = developerName\n                        email = developerEmail\n                    }\n                }\n                scm {\n                    connection = scmConnection\n                    developerConnection = scmDeveloperConnection\n                    url = scmUrl\n                }\n                // A slightly hacky fix so that your POM will include any transitive dependencies\n                // that your library builds upon\n                withXml {\n                    def dependenciesNode = asNode().appendNode('dependencies')\n\n                    project.configurations.implementation.allDependencies.each {\n                        if (it.group != null && it.version != null) {\n                            def dependencyNode = dependenciesNode.appendNode('dependency')\n                            dependencyNode.appendNode('groupId', it.group)\n                            dependencyNode.appendNode('artifactId', it.name)\n                            dependencyNode.appendNode('version', it.version)\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // The repository to publish to, Sonatype/MavenCentral\n    repositories {\n        maven {\n            // This is an arbitrary name, you may also use \"mavencentral\" or\n            // any other name that's descriptive for you\n            name = \"sonatype\"\n            url = \"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/\"\n            credentials {\n                username ossrhUsername\n                password ossrhPassword\n            }\n        }\n    }\n}\n\nsigning {\n    sign publishing.publications\n}\n"
  },
  {
    "path": "cardscan-ui/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": "cardscan-ui/src/androidTest/java/com/getbouncer/cardscan/ui/ExampleInstrumentedTest.kt",
    "content": "package com.getbouncer.cardscan.ui\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport org.junit.Test\nimport kotlin.test.assertEquals\n\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"com.getbouncer.cardscan.ui.test\", appContext.packageName)\n    }\n}\n"
  },
  {
    "path": "cardscan-ui/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.getbouncer.cardscan.ui\">\n\n    <application>\n        <activity\n            android:name=\"com.getbouncer.cardscan.ui.CardScanActivity\"\n            android:screenOrientation=\"nosensor\"\n            android:theme=\"@style/BouncerDefaultTheme\" />\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/CardScanActivity.kt",
    "content": "package com.getbouncer.cardscan.ui\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.view.Gravity\nimport android.view.View\nimport android.widget.ImageView\nimport android.widget.ProgressBar\nimport android.widget.TextView\nimport androidx.constraintlayout.widget.ConstraintLayout\nimport androidx.constraintlayout.widget.ConstraintSet\nimport com.getbouncer.cardscan.ui.analyzer.CompletionLoopAnalyzer\nimport com.getbouncer.cardscan.ui.exception.UnknownScanException\nimport com.getbouncer.cardscan.ui.result.CompletionLoopListener\nimport com.getbouncer.cardscan.ui.result.CompletionLoopResult\nimport com.getbouncer.cardscan.ui.result.MainLoopAggregator\nimport com.getbouncer.scan.framework.AggregateResultListener\nimport com.getbouncer.scan.framework.AnalyzerLoopErrorListener\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.payment.card.getCardIssuer\nimport com.getbouncer.scan.payment.card.isValidExpiry\nimport com.getbouncer.scan.payment.cropCameraPreviewToSquare\nimport com.getbouncer.scan.ui.CancellationReason\nimport com.getbouncer.scan.ui.util.getColorByRes\nimport com.getbouncer.scan.ui.util.hide\nimport com.getbouncer.scan.ui.util.setTextSizeByRes\nimport com.getbouncer.scan.ui.util.setVisible\nimport com.getbouncer.scan.ui.util.show\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\n@Deprecated(\"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class ScannedCard(\n    val pan: String?,\n    val expiryDay: String?,\n    val expiryMonth: String?,\n    val expiryYear: String?,\n    val networkName: String?,\n    val cvc: String?,\n    val cardholderName: String?,\n    val errorString: String?,\n) : Parcelable\n\n@Deprecated(\"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ninterface CardProcessedResultListener : CardScanResultListener {\n\n    /**\n     * A payment card was successfully scanned.\n     */\n    fun cardProcessed(scannedCard: ScannedCard)\n}\n\ninternal const val INTENT_PARAM_REQUEST = \"request\"\ninternal const val INTENT_PARAM_RESULT = \"result\"\n\n@Deprecated(\"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nopen class CardScanActivity :\n    CardScanBaseActivity(),\n    AggregateResultListener<MainLoopAggregator.InterimResult, MainLoopAggregator.FinalResult>,\n    CompletionLoopListener,\n    AnalyzerLoopErrorListener {\n\n    /**\n     * And overlay to darken the screen during result processing.\n     */\n    protected open val processingOverlayView by lazy { View(this) }\n\n    /**\n     * The spinner indicating that results are processing.\n     */\n    protected open val processingSpinnerView by lazy { ProgressBar(this) }\n\n    /**\n     * The text indicating that results are processing\n     */\n    protected open val processingTextView by lazy { TextView(this) }\n\n    /**\n     * The image view for debugging the completion loop\n     */\n    protected open val debugCompletionImageView by lazy { ImageView(this) }\n\n    override fun addUiComponents() {\n        super.addUiComponents()\n        appendUiComponents(processingOverlayView, processingSpinnerView, processingTextView, debugCompletionImageView)\n    }\n\n    override fun setupUiComponents() {\n        super.setupUiComponents()\n\n        setupProcessingOverlayViewUi()\n        setupProcessingTextViewUi()\n        setupDebugCompletionViewUi()\n    }\n\n    protected open fun setupProcessingOverlayViewUi() {\n        processingOverlayView.setBackgroundColor(getColorByRes(R.color.bouncerProcessingBackground))\n    }\n\n    protected open fun setupProcessingTextViewUi() {\n        processingTextView.text = getString(R.string.bouncer_processing_card)\n        processingTextView.setTextSizeByRes(R.dimen.bouncerProcessingTextSize)\n        processingTextView.setTextColor(getColorByRes(R.color.bouncerProcessingText))\n        processingTextView.gravity = Gravity.CENTER\n    }\n\n    protected open fun setupDebugCompletionViewUi() {\n        debugCompletionImageView.contentDescription = getString(R.string.bouncer_debug_description)\n        debugCompletionImageView.setVisible(Config.isDebug)\n    }\n\n    override fun setupUiConstraints() {\n        super.setupUiConstraints()\n\n        setupProcessingOverlayViewConstraints()\n        setupProcessingSpinnerViewConstraints()\n        setupProcessingTextViewConstraints()\n        setupDebugCompletionViewConstraints()\n    }\n\n    protected open fun setupProcessingOverlayViewConstraints() {\n        processingOverlayView.layoutParams = ConstraintLayout.LayoutParams(\n            ConstraintLayout.LayoutParams.MATCH_PARENT, // width\n            ConstraintLayout.LayoutParams.MATCH_PARENT, // height\n        )\n\n        processingOverlayView.constrainToParent()\n    }\n\n    protected open fun setupProcessingSpinnerViewConstraints() {\n        processingSpinnerView.layoutParams = ConstraintLayout.LayoutParams(\n            ConstraintLayout.LayoutParams.WRAP_CONTENT, // width\n            ConstraintLayout.LayoutParams.WRAP_CONTENT, // height\n        )\n\n        processingSpinnerView.constrainToParent()\n    }\n\n    protected open fun setupProcessingTextViewConstraints() {\n        processingTextView.layoutParams = ConstraintLayout.LayoutParams(\n            0, // width\n            ConstraintLayout.LayoutParams.WRAP_CONTENT, // height\n        )\n\n        processingTextView.addConstraints {\n            connect(it.id, ConstraintSet.TOP, processingSpinnerView.id, ConstraintSet.BOTTOM)\n            connect(it.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)\n            connect(it.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)\n        }\n    }\n\n    protected open fun setupDebugCompletionViewConstraints() {\n        debugCompletionImageView.layoutParams = ConstraintLayout.LayoutParams(\n            resources.getDimensionPixelSize(R.dimen.bouncerDebugWindowWidth), // width,\n            resources.getDimensionPixelSize(R.dimen.bouncerDebugWindowWidth), // height\n        )\n\n        debugCompletionImageView.addConstraints {\n            connect(it.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)\n            connect(it.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)\n        }\n    }\n\n    override fun displayState(newState: ScanState, previousState: ScanState?) {\n        super.displayState(newState, previousState)\n\n        when (newState) {\n            is ScanState.NotFound, ScanState.FoundShort, ScanState.FoundLong, ScanState.Wrong -> {\n                processingOverlayView.hide()\n                processingSpinnerView.hide()\n                processingTextView.hide()\n            }\n            is ScanState.Correct -> {\n                processingOverlayView.show()\n                processingSpinnerView.show()\n                processingTextView.show()\n            }\n        }\n    }\n\n    override val scanFlow: CardScanFlow by lazy {\n        CardScanFlow(enableNameExtraction, enableExpiryExtraction, this, this)\n    }\n\n    private val params: CardScanSheetParams by lazy {\n        intent.getParcelableExtra(INTENT_PARAM_REQUEST)\n            ?: CardScanSheetParams(\n                apiKey = \"\",\n                enableEnterManually = true,\n                enableNameExtraction = false,\n                enableExpiryExtraction = false,\n            )\n    }\n\n    override val enableEnterCardManually: Boolean by lazy { params.enableEnterManually }\n\n    override val enableNameExtraction: Boolean by lazy { params.enableNameExtraction }\n\n    override val enableExpiryExtraction: Boolean by lazy { params.enableExpiryExtraction }\n\n    private var pan: String? = null\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        Config.apiKey = params.apiKey\n    }\n\n    override val resultListener = object : CardProcessedResultListener {\n        override fun cardProcessed(scannedCard: ScannedCard) {\n            val intent = Intent()\n                .putExtra(\n                    INTENT_PARAM_RESULT,\n                    CardScanSheetResult.Completed(scannedCard)\n                )\n            setResult(Activity.RESULT_OK, intent)\n            closeScanner()\n        }\n\n        override fun cardScanned(\n            pan: String?,\n            frames: Collection<SavedFrame>,\n            isFastDevice: Boolean\n        ) {\n            this@CardScanActivity.pan = pan\n            launch(Dispatchers.Default) {\n                scanFlow.launchCompletionLoop(\n                    context = this@CardScanActivity,\n                    completionResultListener = this@CardScanActivity,\n                    savedFrames = frames,\n                    isFastDevice = isFastDevice,\n                    coroutineScope = this@CardScanActivity,\n                )\n            }\n        }\n\n        override fun userCanceled(reason: CancellationReason) {\n            val intent = Intent()\n                .putExtra(\n                    INTENT_PARAM_RESULT,\n                    CardScanSheetResult.Canceled(reason),\n                )\n            setResult(Activity.RESULT_CANCELED, intent)\n        }\n\n        override fun failed(cause: Throwable?) {\n            val intent = Intent()\n                .putExtra(\n                    INTENT_PARAM_RESULT,\n                    CardScanSheetResult.Failed(cause ?: UnknownScanException()),\n                )\n            setResult(Activity.RESULT_CANCELED, intent)\n        }\n    }\n\n    override fun onCompletionLoopDone(result: CompletionLoopResult) = launch(Dispatchers.Main) {\n        scanStat.trackResult(\"card_scanned\")\n\n        // Only show the expiry dates that are not expired\n        val (expiryMonth, expiryYear) = if (isValidExpiry(null, result.expiryMonth ?: \"\", result.expiryYear ?: \"\")) {\n            (result.expiryMonth to result.expiryYear)\n        } else {\n            (null to null)\n        }\n\n        resultListener.cardProcessed(\n            scannedCard = ScannedCard(\n                pan = pan,\n                expiryDay = null,\n                expiryMonth = expiryMonth,\n                expiryYear = expiryYear,\n                networkName = getCardIssuer(pan).displayName,\n                cvc = null,\n                cardholderName = result.name,\n                errorString = result.errorString,\n            )\n        )\n    }.let { }\n\n    override fun onCompletionLoopFrameProcessed(\n        result: CompletionLoopAnalyzer.Prediction,\n        frame: SavedFrame,\n    ) = launch(Dispatchers.Main) {\n        if (Config.isDebug) {\n            val bitmap = withContext(Dispatchers.Default) {\n                cropCameraPreviewToSquare(frame.frame.cameraPreviewImage.image.image, frame.frame.cameraPreviewImage.previewImageBounds, frame.frame.cardFinder)\n            }\n            debugCompletionImageView.setImageBitmap(bitmap)\n        }\n    }.let { }\n}\n"
  },
  {
    "path": "cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/CardScanBaseActivity.kt",
    "content": "package com.getbouncer.cardscan.ui\n\nimport android.annotation.SuppressLint\nimport android.os.Bundle\nimport android.util.Size\nimport android.view.Gravity\nimport android.widget.TextView\nimport androidx.constraintlayout.widget.ConstraintLayout\nimport androidx.constraintlayout.widget.ConstraintSet\nimport com.getbouncer.cardscan.ui.result.MainLoopAggregator\nimport com.getbouncer.cardscan.ui.result.MainLoopState\nimport com.getbouncer.scan.framework.AggregateResultListener\nimport com.getbouncer.scan.framework.AnalyzerLoopErrorListener\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.payment.card.formatPan\nimport com.getbouncer.scan.payment.cropCameraPreviewToSquare\nimport com.getbouncer.scan.payment.cropCameraPreviewToViewFinder\nimport com.getbouncer.scan.payment.ml.ssd.DetectionBox\nimport com.getbouncer.scan.ui.CancellationReason\nimport com.getbouncer.scan.ui.DebugDetectionBox\nimport com.getbouncer.scan.ui.ScanResultListener\nimport com.getbouncer.scan.ui.SimpleScanActivity\nimport com.getbouncer.scan.ui.util.getColorByRes\nimport com.getbouncer.scan.ui.util.setTextSizeByRes\nimport com.getbouncer.scan.ui.util.setVisible\nimport com.getbouncer.scan.ui.util.show\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.withContext\nimport java.util.concurrent.atomic.AtomicBoolean\n\n@Deprecated(\"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ninterface CardScanResultListener : ScanResultListener {\n\n    /**\n     * A payment card was successfully scanned.\n     */\n    fun cardScanned(\n        pan: String?,\n        frames: Collection<SavedFrame>,\n        isFastDevice: Boolean,\n    )\n}\n\nprivate val MINIMUM_RESOLUTION = Size(1067, 600) // minimum size of screen detect\n\nprivate fun DetectionBox.forDebug() = DebugDetectionBox(rect, confidence, label.toString())\n\n@Deprecated(\"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nabstract class CardScanBaseActivity :\n    SimpleScanActivity(),\n    AggregateResultListener<MainLoopAggregator.InterimResult, MainLoopAggregator.FinalResult>,\n    AnalyzerLoopErrorListener {\n\n    /**\n     * The text view that lets a user manually enter a card.\n     */\n    protected open val enterCardManuallyTextView: TextView by lazy { TextView(this) }\n\n    protected abstract val enableEnterCardManually: Boolean\n    protected abstract val enableNameExtraction: Boolean\n    protected abstract val enableExpiryExtraction: Boolean\n\n    /**\n     * The listener which handles results from the scan.\n     */\n    abstract override val resultListener: CardScanResultListener\n\n    private var mainLoopIsProducingResults = AtomicBoolean(false)\n    private val hasPreviousValidResult = AtomicBoolean(false)\n\n    abstract override val scanFlow: CardScanFlow\n\n    override val minimumAnalysisResolution: Size = MINIMUM_RESOLUTION\n\n    /**\n     * During on create\n     */\n    @SuppressLint(\"ClickableViewAccessibility\")\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        enterCardManuallyTextView.setOnClickListener { enterCardManually() }\n    }\n\n    override fun addUiComponents() {\n        super.addUiComponents()\n        appendUiComponents(enterCardManuallyTextView)\n    }\n\n    override fun setupUiComponents() {\n        super.setupUiComponents()\n\n        enterCardManuallyTextView.text = getString(R.string.bouncer_enter_card_manually)\n        enterCardManuallyTextView.setTextSizeByRes(R.dimen.bouncerEnterCardManuallyTextSize)\n        enterCardManuallyTextView.gravity = Gravity.CENTER\n\n        enterCardManuallyTextView.setVisible(enableEnterCardManually)\n\n        if (isBackgroundDark()) {\n            enterCardManuallyTextView.setTextColor(getColorByRes(R.color.bouncerEnterCardManuallyColorDark))\n        } else {\n            enterCardManuallyTextView.setTextColor(getColorByRes(R.color.bouncerEnterCardManuallyColorLight))\n        }\n    }\n\n    override fun setupUiConstraints() {\n        super.setupUiConstraints()\n\n        enterCardManuallyTextView.layoutParams = ConstraintLayout.LayoutParams(\n            ConstraintLayout.LayoutParams.WRAP_CONTENT, // width\n            ConstraintLayout.LayoutParams.WRAP_CONTENT, // height\n        ).apply {\n            marginStart = resources.getDimensionPixelSize(R.dimen.bouncerEnterCardManuallyMargin)\n            marginEnd = resources.getDimensionPixelSize(R.dimen.bouncerEnterCardManuallyMargin)\n            bottomMargin = resources.getDimensionPixelSize(R.dimen.bouncerEnterCardManuallyMargin)\n            topMargin = resources.getDimensionPixelSize(R.dimen.bouncerEnterCardManuallyMargin)\n        }\n\n        enterCardManuallyTextView.addConstraints {\n            connect(it.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)\n            connect(it.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)\n            connect(it.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)\n        }\n    }\n\n    /**\n     * Cancel scanning to enter a card manually\n     */\n    protected open fun enterCardManually() {\n        runBlocking { scanStat.trackResult(\"enter_card_manually\") }\n        resultListener.userCanceled(CancellationReason.UserCannotScan)\n        closeScanner()\n    }\n\n    /**\n     * A final result was received from the aggregator.\n     */\n    override suspend fun onResult(result: MainLoopAggregator.FinalResult) = launch(Dispatchers.Main) {\n        changeScanState(ScanState.Correct)\n        cameraAdapter.unbindFromLifecycle(this@CardScanBaseActivity)\n        resultListener.cardScanned(\n            pan = result.pan,\n            frames = scanFlow.selectCompletionLoopFrames(result.averageFrameRate, result.savedFrames),\n            isFastDevice = result.averageFrameRate > Config.slowDeviceFrameRate,\n        )\n    }.let { }\n\n    /**\n     * An interim result was received from the result aggregator.\n     */\n    override suspend fun onInterimResult(result: MainLoopAggregator.InterimResult) = launch(Dispatchers.Main) {\n        if (!mainLoopIsProducingResults.getAndSet(true)) {\n            scanStat.trackResult(\"first_image_processed\")\n        }\n        if (result.state is MainLoopState.PanFound && !hasPreviousValidResult.getAndSet(true)) {\n            scanStat.trackResult(\"ocr_pan_observed\")\n        }\n\n        if (Config.displayScanResult) {\n            if (Config.isDebug && result.analyzerResult.ocr?.pan?.isNotEmpty() == true) {\n                cardNumberTextView.text = formatPan(result.analyzerResult.ocr.pan)\n                cardNumberTextView.show()\n            } else {\n                val mostLikelyPan = when (val state = result.state) {\n                    is MainLoopState.Initial -> null\n                    is MainLoopState.PanFound -> state.getMostLikelyPan()\n                    is MainLoopState.PanSatisfied -> state.pan\n                    is MainLoopState.CardSatisfied -> state.getMostLikelyPan()\n                    is MainLoopState.Finished -> state.pan\n                }\n                if (mostLikelyPan?.isNotEmpty() == true) {\n                    cardNumberTextView.text = formatPan(mostLikelyPan)\n                    cardNumberTextView.show()\n                }\n            }\n        }\n\n        when (result.state) {\n            is MainLoopState.Initial -> if (scanState !is ScanState.FoundLong) changeScanState(ScanState.NotFound)\n            is MainLoopState.PanFound -> changeScanState(ScanState.FoundLong)\n            is MainLoopState.PanSatisfied -> changeScanState(ScanState.FoundLong)\n            is MainLoopState.CardSatisfied -> changeScanState(ScanState.FoundLong)\n            is MainLoopState.Finished -> changeScanState(ScanState.Correct)\n        }\n\n        if (Config.isDebug) {\n            result.analyzerResult.ocr?.detectedBoxes?.let { detectionBoxes ->\n                val bitmap = withContext(Dispatchers.Default) {\n                    cropCameraPreviewToViewFinder(\n                        result.frame.cameraPreviewImage.image.image,\n                        result.frame.cameraPreviewImage.previewImageBounds,\n                        result.frame.cardFinder\n                    )\n                }\n                debugImageView.setImageBitmap(bitmap)\n                debugOverlayView.setBoxes(detectionBoxes.map { it.forDebug() })\n            } ?: run {\n                val bitmap = withContext(Dispatchers.Default) {\n                    cropCameraPreviewToSquare(\n                        result.frame.cameraPreviewImage.image.image,\n                        result.frame.cameraPreviewImage.previewImageBounds,\n                        result.frame.cardFinder\n                    )\n                }\n                debugImageView.setImageBitmap(bitmap)\n                debugOverlayView.clearBoxes()\n            }\n        }\n    }.let { }\n\n    override suspend fun onReset() = launch(Dispatchers.Main) { changeScanState(ScanState.NotFound) }.let { }\n\n    override fun onAnalyzerFailure(t: Throwable): Boolean {\n        analyzerFailureCancelScan(t)\n        return true\n    }\n\n    override fun onResultFailure(t: Throwable): Boolean {\n        analyzerFailureCancelScan(t)\n        return true\n    }\n}\n"
  },
  {
    "path": "cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/CardScanFlow.kt",
    "content": "package com.getbouncer.cardscan.ui\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.Rect\nimport android.util.Log\nimport androidx.annotation.RestrictTo\nimport androidx.lifecycle.LifecycleOwner\nimport com.getbouncer.cardscan.ui.analyzer.CompletionLoopAnalyzer\nimport com.getbouncer.cardscan.ui.analyzer.MainLoopAnalyzer\nimport com.getbouncer.cardscan.ui.result.CompletionLoopAggregator\nimport com.getbouncer.cardscan.ui.result.CompletionLoopListener\nimport com.getbouncer.cardscan.ui.result.CompletionLoopResult\nimport com.getbouncer.cardscan.ui.result.MainLoopAggregator\nimport com.getbouncer.cardscan.ui.result.MainLoopState\nimport com.getbouncer.scan.camera.CameraAdapter\nimport com.getbouncer.scan.camera.CameraPreviewImage\nimport com.getbouncer.scan.framework.AggregateResultListener\nimport com.getbouncer.scan.framework.AnalyzerLoopErrorListener\nimport com.getbouncer.scan.framework.AnalyzerPool\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.FetchedData\nimport com.getbouncer.scan.framework.FiniteAnalyzerLoop\nimport com.getbouncer.scan.framework.ProcessBoundAnalyzerLoop\nimport com.getbouncer.scan.framework.time.Duration\nimport com.getbouncer.scan.framework.time.Rate\nimport com.getbouncer.scan.payment.FrameDetails\nimport com.getbouncer.scan.payment.TextDetectModelManager\nimport com.getbouncer.scan.payment.analyzer.NameAndExpiryAnalyzer\nimport com.getbouncer.scan.payment.ml.AlphabetDetect\nimport com.getbouncer.scan.payment.ml.AlphabetDetectModelManager\nimport com.getbouncer.scan.payment.ml.CardDetect\nimport com.getbouncer.scan.payment.ml.CardDetectModelManager\nimport com.getbouncer.scan.payment.ml.ExpiryDetect\nimport com.getbouncer.scan.payment.ml.ExpiryDetectModelManager\nimport com.getbouncer.scan.payment.ml.SSDOcr\nimport com.getbouncer.scan.payment.ml.SSDOcrModelManager\nimport com.getbouncer.scan.payment.ml.TextDetect\nimport com.getbouncer.scan.ui.ScanFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.withContext\n\n@Deprecated(\"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class SavedFrame(\n    val pan: String?,\n    val frame: MainLoopAnalyzer.Input,\n    val details: FrameDetails,\n)\n\n@Deprecated(\"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class SavedFrameType(\n    val hasCard: Boolean,\n    val hasPan: Boolean,\n)\n\n/**\n * This class contains the logic required for analyzing a credit card for scanning.\n */\n@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)\n@Deprecated(\"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nopen class CardScanFlow(\n    private val enableNameExtraction: Boolean,\n    private val enableExpiryExtraction: Boolean,\n    private val scanResultListener: AggregateResultListener<MainLoopAggregator.InterimResult, MainLoopAggregator.FinalResult>,\n    private val scanErrorListener: AnalyzerLoopErrorListener,\n) : ScanFlow {\n    companion object {\n        private const val MAX_COMPLETION_LOOP_FRAMES_FAST_DEVICE = 8\n        private const val MAX_COMPLETION_LOOP_FRAMES_SLOW_DEVICE = 5\n\n        /**\n         * Warm up the analyzers for card scanner. This method is optional, but will increase the speed at which the\n         * scan occurs.\n         *\n         * @param context: A context to use for warming up the analyzers.\n         */\n        @JvmStatic\n        @Deprecated(\"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n        suspend fun prepareScan(\n            context: Context,\n            apiKey: String,\n            initializeNameAndExpiryExtraction: Boolean,\n            forImmediateUse: Boolean,\n        ) = withContext(Dispatchers.IO) {\n            Config.apiKey = apiKey\n            val deferredFetchers = mutableListOf<Deferred<FetchedData>>()\n\n            deferredFetchers.add(async { SSDOcrModelManager.fetchModel(context, forImmediateUse) })\n            deferredFetchers.add(async { CardDetectModelManager.fetchModel(context, forImmediateUse) })\n\n            if (initializeNameAndExpiryExtraction) {\n                deferredFetchers.add(async { TextDetectModelManager.fetchModel(context, forImmediateUse) })\n                deferredFetchers.add(async { AlphabetDetectModelManager.fetchModel(context, forImmediateUse) })\n                deferredFetchers.add(async { ExpiryDetectModelManager.fetchModel(context, forImmediateUse) })\n            }\n\n            deferredFetchers.fold(true) { acc, deferred -> acc && deferred.await().successfullyFetched }\n        }\n\n        /**\n         * Determine if the scan is supported\n         */\n        @JvmStatic\n        @Deprecated(\n            message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n            replaceWith = ReplaceWith(\"StripeCardScan\")\n        )\n        fun isSupported(context: Context) = CameraAdapter.isCameraSupported(context)\n\n        /**\n         * Determine if the scan models are available (have been warmed up)\n         */\n        @JvmStatic\n        @Deprecated(\n            message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n            replaceWith = ReplaceWith(\"StripeCardScan\")\n        )\n        fun isScanReady() = runBlocking { SSDOcrModelManager.isReady() && CardDetectModelManager.isReady() }\n    }\n\n    /**\n     * If this is true, do not start the flow.\n     */\n    private var canceled = false\n\n    private var mainLoopAnalyzerPool: AnalyzerPool<MainLoopAnalyzer.Input, MainLoopState, MainLoopAnalyzer.Prediction>? = null\n    private var mainLoopAggregator: MainLoopAggregator? = null\n    private var mainLoop: ProcessBoundAnalyzerLoop<MainLoopAnalyzer.Input, MainLoopState, MainLoopAnalyzer.Prediction>? = null\n    private var mainLoopJob: Job? = null\n\n    private var completionLoopAnalyzerPool: AnalyzerPool<SavedFrame, Unit, CompletionLoopAnalyzer.Prediction>? = null\n    private var completionLoop: FiniteAnalyzerLoop<SavedFrame, Unit, CompletionLoopAnalyzer.Prediction>? = null\n    private var completionLoopJob: Job? = null\n\n    /**\n     * Start the image processing flow for scanning a card.\n     *\n     * @param context: The context used to download analyzers if needed\n     * @param imageStream: The flow of images to process\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\")\n    )\n    override fun startFlow(\n        context: Context,\n        imageStream: Flow<CameraPreviewImage<Bitmap>>,\n        viewFinder: Rect,\n        lifecycleOwner: LifecycleOwner,\n        coroutineScope: CoroutineScope\n    ) = coroutineScope.launch(Dispatchers.Main) {\n        val listener =\n            object : AggregateResultListener<MainLoopAggregator.InterimResult, MainLoopAggregator.FinalResult> {\n                override suspend fun onResult(result: MainLoopAggregator.FinalResult) {\n                    mainLoop?.unsubscribe()\n                    mainLoop = null\n\n                    mainLoopJob?.apply { if (isActive) { cancel() } }\n                    mainLoopJob = null\n\n                    mainLoopAggregator = null\n\n                    mainLoopAnalyzerPool?.closeAllAnalyzers()\n                    mainLoopAnalyzerPool = null\n\n                    scanResultListener.onResult(result)\n                }\n\n                override suspend fun onInterimResult(result: MainLoopAggregator.InterimResult) {\n                    scanResultListener.onInterimResult(result)\n                }\n\n                override suspend fun onReset() {\n                    scanResultListener.onReset()\n                }\n            }\n\n        if (canceled) {\n            return@launch\n        }\n\n        mainLoopAggregator = MainLoopAggregator(listener).also { aggregator ->\n            // make this result aggregator pause and reset when the lifecycle pauses.\n            aggregator.bindToLifecycle(lifecycleOwner)\n\n            val analyzerPool = AnalyzerPool.of(\n                MainLoopAnalyzer.Factory(\n                    SSDOcr.Factory(context, SSDOcrModelManager.fetchModel(context, forImmediateUse = true, isOptional = false)),\n                    CardDetect.Factory(context, CardDetectModelManager.fetchModel(context, forImmediateUse = true, isOptional = false)),\n                )\n            )\n            mainLoopAnalyzerPool = analyzerPool\n\n            mainLoop = ProcessBoundAnalyzerLoop(\n                analyzerPool = analyzerPool,\n                resultHandler = aggregator,\n                analyzerLoopErrorListener = scanErrorListener,\n            ).apply {\n                mainLoopJob = subscribeTo(\n                    imageStream.map {\n                        MainLoopAnalyzer.Input(\n                            cameraPreviewImage = it,\n                            cardFinder = viewFinder,\n                        )\n                    },\n                    coroutineScope,\n                )\n            }\n        }\n    }.let { }\n\n    /**\n     * In the event that the scan cannot complete, halt the flow to halt analyzers and free up CPU and memory.\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\")\n    )\n    override fun cancelFlow() {\n        canceled = true\n\n        mainLoopAggregator?.run { cancel() }\n        mainLoopAggregator = null\n\n        mainLoop?.unsubscribe()\n        mainLoop = null\n\n        mainLoopAnalyzerPool?.closeAllAnalyzers()\n        mainLoopAnalyzerPool = null\n\n        mainLoopJob?.apply { if (isActive) { cancel() } }\n        mainLoopJob = null\n\n        completionLoop?.cancel()\n        completionLoop = null\n\n        completionLoopAnalyzerPool?.closeAllAnalyzers()\n        completionLoopAnalyzerPool = null\n\n        completionLoopJob?.apply { if (isActive) { cancel() } }\n        completionLoopJob = null\n    }\n\n    open fun launchCompletionLoop(\n        context: Context,\n        completionResultListener: CompletionLoopListener,\n        savedFrames: Collection<SavedFrame>,\n        isFastDevice: Boolean,\n        coroutineScope: CoroutineScope,\n    ) = coroutineScope.launch {\n        if (canceled) {\n            return@launch\n        }\n\n        val analyzerPool = AnalyzerPool.of(\n            CompletionLoopAnalyzer.Factory(\n                nameAndExpiryFactory = NameAndExpiryAnalyzer.Factory(\n                    textDetectFactory = TextDetect.Factory(\n                        context,\n                        TextDetectModelManager.fetchModel(context, forImmediateUse = true, isOptional = true)\n                    ),\n                    alphabetDetectFactory = AlphabetDetect.Factory(\n                        context,\n                        AlphabetDetectModelManager.fetchModel(context, forImmediateUse = true, isOptional = true)\n                    ),\n                    expiryDetectFactory = ExpiryDetect.Factory(\n                        context,\n                        ExpiryDetectModelManager.fetchModel(context, forImmediateUse = true, isOptional = true)\n                    ),\n                    runNameExtraction = enableNameExtraction && isFastDevice,\n                    runExpiryExtraction = enableExpiryExtraction,\n                )\n            )\n        )\n        completionLoopAnalyzerPool = analyzerPool\n\n        completionLoop = FiniteAnalyzerLoop(\n            analyzerPool = analyzerPool,\n            resultHandler = CompletionLoopAggregator(object : CompletionLoopListener {\n                override fun onCompletionLoopDone(result: CompletionLoopResult) {\n                    completionLoop = null\n\n                    completionLoopAnalyzerPool?.closeAllAnalyzers()\n                    completionLoopAnalyzerPool = null\n\n                    completionLoopJob?.apply { if (isActive) { cancel() } }\n                    completionLoopJob = null\n\n                    completionResultListener.onCompletionLoopDone(result)\n                }\n\n                override fun onCompletionLoopFrameProcessed(\n                    result: CompletionLoopAnalyzer.Prediction,\n                    frame: SavedFrame\n                ) = completionResultListener.onCompletionLoopFrameProcessed(result, frame)\n            }),\n            analyzerLoopErrorListener = object : AnalyzerLoopErrorListener {\n                override fun onAnalyzerFailure(t: Throwable): Boolean {\n                    Log.e(Config.logTag, \"Completion loop analyzer failure\", t)\n                    completionResultListener.onCompletionLoopDone(CompletionLoopResult())\n                    return true // terminate the loop on any analyzer failures\n                }\n\n                override fun onResultFailure(t: Throwable): Boolean {\n                    Log.e(Config.logTag, \"Completion loop result failures\", t)\n                    completionResultListener.onCompletionLoopDone(CompletionLoopResult())\n                    return true // terminate the loop on any result failures\n                }\n            }\n        ).apply {\n            completionLoopJob = process(savedFrames, coroutineScope)\n        }\n    }.let { }\n\n    /**\n     * Select which frames to use in the completion loop.\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\")\n    )\n    open fun <SavedFrame> selectCompletionLoopFrames(\n        frameRate: Rate,\n        frames: Map<SavedFrameType, List<SavedFrame>>,\n    ): Collection<SavedFrame> {\n        fun getFrames(frameType: SavedFrameType) = frames[frameType] ?: emptyList()\n\n        val cardAndPan = getFrames(SavedFrameType(hasCard = true, hasPan = true))\n        val card = getFrames(SavedFrameType(hasCard = true, hasPan = false))\n        val pan = getFrames(SavedFrameType(hasCard = false, hasPan = true))\n\n        val maxCompletionLoopFrames =\n            if (frameRate.duration <= Duration.ZERO || frameRate > Config.slowDeviceFrameRate) {\n                MAX_COMPLETION_LOOP_FRAMES_FAST_DEVICE\n            } else {\n                MAX_COMPLETION_LOOP_FRAMES_SLOW_DEVICE\n            }\n\n        return (cardAndPan + card + pan).take(maxCompletionLoopFrames)\n    }\n}\n"
  },
  {
    "path": "cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/CardScanSheet.kt",
    "content": "package com.getbouncer.cardscan.ui\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Parcelable\nimport androidx.activity.ComponentActivity\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.activity.result.ActivityResultRegistry\nimport androidx.activity.result.contract.ActivityResultContract\nimport androidx.fragment.app.Fragment\nimport com.getbouncer.cardscan.ui.exception.UnknownScanException\nimport com.getbouncer.scan.ui.CancellationReason\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.launch\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\ninternal data class CardScanSheetParams(\n    val apiKey: String,\n    val enableEnterManually: Boolean,\n    val enableNameExtraction: Boolean,\n    val enableExpiryExtraction: Boolean,\n) : Parcelable\n\n@Deprecated(\"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nsealed interface CardScanSheetResult : Parcelable {\n\n    @Parcelize\n    data class Completed(\n        val scannedCard: ScannedCard,\n    ) : CardScanSheetResult\n\n    @Parcelize\n    data class Canceled(\n        val reason: CancellationReason,\n    ) : CardScanSheetResult\n\n    @Parcelize\n    data class Failed(val error: Throwable) : CardScanSheetResult\n}\n\n/**\n * @Deprecated in favor of Stripe CardScan. This code was migrated to Stripe CardScan and is no\n * longer maintained in this repository. For the up-to-date version of this SDK, please visit\n * https://github.com/stripe/stripe-android/tree/master/stripecardscan\n */\n@Deprecated(\"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass CardScanSheet private constructor(private val apiKey: String) {\n\n    private lateinit var launcher: ActivityResultLauncher<CardScanSheetParams>\n\n    /**\n     * Callback to notify when scanning finishes and a result is available.\n     */\n    fun interface CardScanResultCallback {\n        fun onCardScanSheetResult(cardScanSheetResult: CardScanSheetResult)\n    }\n\n    companion object {\n        /**\n         * Create a [CardScanSheet] instance with [ComponentActivity].\n         *\n         * This API registers an [ActivityResultLauncher] into the\n         * [ComponentActivity], it must be called before the [ComponentActivity]\n         * is created (in the onCreate method).\n         */\n        @JvmStatic\n        @JvmOverloads\n        @Deprecated(\"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n        fun create(\n            from: ComponentActivity,\n            apiKey: String,\n            cardScanResultCallback: CardScanResultCallback,\n            registry: ActivityResultRegistry = from.activityResultRegistry,\n        ) = CardScanSheet(apiKey).apply {\n            launcher = from.registerForActivityResult(\n                activityResultContract,\n                registry,\n                cardScanResultCallback::onCardScanSheetResult,\n            )\n        }\n\n        @JvmStatic\n        @JvmOverloads\n        @Deprecated(\"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n        fun create(\n            from: Fragment,\n            apiKey: String,\n            cardScanResultCallback: CardScanResultCallback,\n            registry: ActivityResultRegistry? = null,\n        ) = CardScanSheet(apiKey).apply {\n            launcher = if (registry != null) {\n                from.registerForActivityResult(\n                    activityResultContract,\n                    registry,\n                    cardScanResultCallback::onCardScanSheetResult,\n                )\n            } else {\n                from.registerForActivityResult(\n                    activityResultContract,\n                    cardScanResultCallback::onCardScanSheetResult,\n                )\n            }\n        }\n\n        private val activityResultContract =\n            object : ActivityResultContract<CardScanSheetParams, CardScanSheetResult>() {\n                override fun createIntent(\n                    context: Context,\n                    input: CardScanSheetParams,\n                ) = this@Companion.createIntent(context, input)\n\n                override fun parseResult(\n                    resultCode: Int,\n                    intent: Intent?,\n                ) = this@Companion.parseResult(intent)\n            }\n\n        private fun createIntent(context: Context, input: CardScanSheetParams) =\n            Intent(context, CardScanActivity::class.java)\n                .putExtra(INTENT_PARAM_REQUEST, input)\n\n        private fun parseResult(intent: Intent?): CardScanSheetResult =\n            intent?.getParcelableExtra(INTENT_PARAM_RESULT)\n                ?: CardScanSheetResult.Failed(\n                    UnknownScanException(\"No data in the result intent\")\n                )\n\n        /**\n         * Determine if the scan is supported\n         */\n        @JvmStatic\n        @Deprecated(\n            message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n            replaceWith = ReplaceWith(\"StripeCardScan\")\n        )\n        fun isSupported(context: Context) = CardScanFlow.isSupported(context)\n\n        /**\n         * Determine if the scan models are available (have been warmed up)\n         */\n        @JvmStatic\n        @Deprecated(\n            message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n            replaceWith = ReplaceWith(\"StripeCardScan\")\n        )\n        fun isScanReady() = CardScanFlow.isScanReady()\n\n        /**\n         * Warm up the analyzers and call [onPrepared] once the scan is ready.\n         *\n         * @param context: A context to use for warming up the analyzers.\n         * @param apiKey: the API key used to warm up the ML models\n         * @param initializeNameAndExpiryExtraction: if true, include name and expiry extraction\n         * @param onPrepared: called once the scan is ready\n         */\n        @JvmStatic\n        @Deprecated(\n            message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n            replaceWith = ReplaceWith(\"StripeCardScan\")\n        )\n        fun prepareScan(\n            context: Context,\n            apiKey: String,\n            initializeNameAndExpiryExtraction: Boolean,\n            onPrepared: () -> Unit,\n        ) = GlobalScope.launch {\n            CardScanFlow.prepareScan(context, apiKey, initializeNameAndExpiryExtraction, false)\n        }.invokeOnCompletion { onPrepared() }\n    }\n\n    /**\n     * Present the CardScan flow.\n     * Results will be returned in the callback function.\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\")\n    )\n    fun present(\n        enableEnterManually: Boolean,\n        enableNameExtraction: Boolean,\n        enableExpiryExtraction: Boolean,\n    ) {\n        launcher.launch(\n            CardScanSheetParams(\n                apiKey = apiKey,\n                enableEnterManually = enableEnterManually,\n                enableNameExtraction = enableNameExtraction,\n                enableExpiryExtraction = enableExpiryExtraction,\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/analyzer/CompletionLoopAnalyzer.kt",
    "content": "package com.getbouncer.cardscan.ui.analyzer\n\nimport com.getbouncer.cardscan.ui.SavedFrame\nimport com.getbouncer.scan.framework.Analyzer\nimport com.getbouncer.scan.framework.AnalyzerFactory\nimport com.getbouncer.scan.payment.analyzer.NameAndExpiryAnalyzer\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass CompletionLoopAnalyzer private constructor(\n    private val nameAndExpiryAnalyzer: NameAndExpiryAnalyzer?,\n) : Analyzer<SavedFrame, Unit, CompletionLoopAnalyzer.Prediction> {\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    class Prediction(\n        val nameAndExpiryResult: NameAndExpiryAnalyzer.Prediction?,\n        val isNameExtractionAvailable: Boolean,\n        val isExpiryExtractionAvailable: Boolean,\n        val enableNameExtraction: Boolean,\n        val enableExpiryExtraction: Boolean,\n    )\n\n    override suspend fun analyze(\n        data: SavedFrame,\n        state: Unit,\n    ) = Prediction(\n        nameAndExpiryResult = nameAndExpiryAnalyzer?.analyze(\n            NameAndExpiryAnalyzer.Input(data.frame.cameraPreviewImage.image, data.frame.cameraPreviewImage.previewImageBounds, data.frame.cardFinder),\n            state,\n        ),\n        isNameExtractionAvailable = nameAndExpiryAnalyzer?.isNameDetectorAvailable() ?: false,\n        isExpiryExtractionAvailable = nameAndExpiryAnalyzer?.isExpiryDetectorAvailable() ?: false,\n        enableNameExtraction = nameAndExpiryAnalyzer?.runNameExtraction ?: false,\n        enableExpiryExtraction = nameAndExpiryAnalyzer?.runExpiryExtraction ?: false,\n    )\n\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    class Factory(\n        private val nameAndExpiryFactory: AnalyzerFactory<NameAndExpiryAnalyzer.Input, Any, NameAndExpiryAnalyzer.Prediction, out NameAndExpiryAnalyzer>,\n    ) : AnalyzerFactory<SavedFrame, Unit, Prediction, CompletionLoopAnalyzer> {\n        override suspend fun newInstance() = CompletionLoopAnalyzer(\n            nameAndExpiryAnalyzer = nameAndExpiryFactory.newInstance(),\n        )\n    }\n}\n"
  },
  {
    "path": "cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/analyzer/MainLoopAnalyzer.kt",
    "content": "package com.getbouncer.cardscan.ui.analyzer\n\nimport android.graphics.Bitmap\nimport android.graphics.Rect\nimport com.getbouncer.cardscan.ui.result.MainLoopState\nimport com.getbouncer.scan.camera.CameraPreviewImage\nimport com.getbouncer.scan.framework.Analyzer\nimport com.getbouncer.scan.framework.AnalyzerFactory\nimport com.getbouncer.scan.payment.ml.CardDetect\nimport com.getbouncer.scan.payment.ml.SSDOcr\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass MainLoopAnalyzer(\n    private val ssdOcr: Analyzer<SSDOcr.Input, Any, SSDOcr.Prediction>?,\n    private val cardDetect: Analyzer<CardDetect.Input, Any, CardDetect.Prediction>?,\n) : Analyzer<MainLoopAnalyzer.Input, MainLoopState, MainLoopAnalyzer.Prediction> {\n\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    data class Input(\n        val cameraPreviewImage: CameraPreviewImage<Bitmap>,\n        val cardFinder: Rect,\n    )\n\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    class Prediction(\n        val ocr: SSDOcr.Prediction?,\n        val card: CardDetect.Prediction?,\n    ) {\n        val isCardVisible = card?.side?.let { it == CardDetect.Prediction.Side.NO_PAN || it == CardDetect.Prediction.Side.PAN }\n    }\n\n    override suspend fun analyze(data: Input, state: MainLoopState): Prediction {\n        val cardResult = if (state.runCardDetect) cardDetect?.analyze(CardDetect.cameraPreviewToInput(data.cameraPreviewImage.image, data.cameraPreviewImage.previewImageBounds, data.cardFinder), Unit) else null\n        val ocrResult = if (state.runOcr) ssdOcr?.analyze(SSDOcr.cameraPreviewToInput(data.cameraPreviewImage.image, data.cameraPreviewImage.previewImageBounds, data.cardFinder), Unit) else null\n\n        return Prediction(\n            ocr = ocrResult,\n            card = cardResult,\n        )\n    }\n\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    class Factory(\n        private val ssdOcrFactory: AnalyzerFactory<SSDOcr.Input, out Any, SSDOcr.Prediction, out Analyzer<SSDOcr.Input, Any, SSDOcr.Prediction>>,\n        private val cardDetectFactory: AnalyzerFactory<CardDetect.Input, out Any, CardDetect.Prediction, out Analyzer<CardDetect.Input, Any, CardDetect.Prediction>>,\n    ) : AnalyzerFactory<Input, MainLoopState, Prediction, MainLoopAnalyzer> {\n        override suspend fun newInstance(): MainLoopAnalyzer = MainLoopAnalyzer(\n            ssdOcr = ssdOcrFactory.newInstance(),\n            cardDetect = cardDetectFactory.newInstance(),\n        )\n    }\n}\n"
  },
  {
    "path": "cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/exception/StripeNetworkException.kt",
    "content": "package com.getbouncer.cardscan.ui.exception\n\nimport java.lang.Exception\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass StripeNetworkException(message: String) : Exception(message)\n"
  },
  {
    "path": "cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/exception/UnknownScanException.kt",
    "content": "package com.getbouncer.cardscan.ui.exception\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass UnknownScanException(message: String? = null) : Exception(message)\n"
  },
  {
    "path": "cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/result/CompletionLoopAggregator.kt",
    "content": "package com.getbouncer.cardscan.ui.result\n\nimport com.getbouncer.cardscan.ui.SavedFrame\nimport com.getbouncer.cardscan.ui.analyzer.CompletionLoopAnalyzer\nimport com.getbouncer.scan.framework.TerminatingResultHandler\nimport com.getbouncer.scan.framework.time.Duration\nimport com.getbouncer.scan.framework.util.FrameRateTracker\nimport com.getbouncer.scan.framework.util.ItemCounter\nimport com.getbouncer.scan.framework.util.ItemTotalCounter\nimport com.getbouncer.scan.payment.ml.ExpiryDetect\n\nprivate const val MINIMUM_NAME_AGREEMENT = 2\nprivate const val MINIMUM_EXPIRY_AGREEMENT = 2\n\nprivate const val INSUFFICIENT_PERMISSIONS_PREFIX = \"Insufficient API key permissions - \"\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ninterface CompletionLoopListener {\n    fun onCompletionLoopDone(result: CompletionLoopResult)\n\n    fun onCompletionLoopFrameProcessed(\n        result: CompletionLoopAnalyzer.Prediction,\n        frame: SavedFrame,\n    )\n}\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class CompletionLoopResult(\n    val name: String? = null,\n    val expiryMonth: String? = null,\n    val expiryYear: String? = null,\n    val errorString: String? = null,\n)\n\n/**\n * Collect the results from executing the completion loop across multiple saved images. Send the\n * collected results to the [listener].\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass CompletionLoopAggregator(\n    private val listener: CompletionLoopListener,\n) : TerminatingResultHandler<SavedFrame, Unit, CompletionLoopAnalyzer.Prediction>(Unit) {\n\n    private val frameRateTracker by lazy {\n        FrameRateTracker(\"cardscan_completion_loop_aggregator\", notifyInterval = Duration.ZERO)\n    }\n\n    private val nameCounter: ItemCounter<String> = ItemTotalCounter()\n    private val expiryCounter: ItemCounter<ExpiryDetect.Expiry> = ItemTotalCounter()\n    private val errors = mutableSetOf<String>()\n\n    override suspend fun onResult(\n        result: CompletionLoopAnalyzer.Prediction,\n        data: SavedFrame,\n    ) {\n        result.nameAndExpiryResult?.let { prediction ->\n            prediction.name?.let { nameCounter.countItem(it) }\n            prediction.expiry?.let { expiryCounter.countItem(it) }\n        }\n\n        if (!result.isNameExtractionAvailable && result.enableNameExtraction) {\n            errors.add(\"name\")\n        }\n\n        if (!result.isExpiryExtractionAvailable && result.enableExpiryExtraction) {\n            errors.add(\"expiry\")\n        }\n\n        frameRateTracker.trackFrameProcessed()\n\n        listener.onCompletionLoopFrameProcessed(result, data)\n    }\n\n    override suspend fun onAllDataProcessed() {\n        val expiry = expiryCounter.getHighestCountItem(MINIMUM_EXPIRY_AGREEMENT)?.second\n        listener.onCompletionLoopDone(\n            CompletionLoopResult(\n                name = nameCounter.getHighestCountItem(MINIMUM_NAME_AGREEMENT)?.second,\n                expiryMonth = expiry?.month,\n                expiryYear = expiry?.year,\n                errorString = if (errors.isNotEmpty()) {\n                    INSUFFICIENT_PERMISSIONS_PREFIX + errors.joinToString(\",\", prefix = \"[\", postfix = \"]\")\n                } else null,\n            )\n        )\n    }\n\n    override suspend fun onTerminatedEarly() {\n        val expiry = expiryCounter.getHighestCountItem(MINIMUM_EXPIRY_AGREEMENT)?.second\n        listener.onCompletionLoopDone(\n            CompletionLoopResult(\n                name = nameCounter.getHighestCountItem(MINIMUM_NAME_AGREEMENT)?.second,\n                expiryMonth = expiry?.month,\n                expiryYear = expiry?.year,\n                errorString = if (errors.isNotEmpty()) {\n                    INSUFFICIENT_PERMISSIONS_PREFIX + errors.joinToString(\",\", prefix = \"[\", postfix = \"]\")\n                } else null,\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/result/MainLoopAggregator.kt",
    "content": "package com.getbouncer.cardscan.ui.result\n\nimport android.util.Log\nimport androidx.annotation.Keep\nimport com.getbouncer.cardscan.ui.SavedFrame\nimport com.getbouncer.cardscan.ui.SavedFrameType\nimport com.getbouncer.cardscan.ui.analyzer.MainLoopAnalyzer\nimport com.getbouncer.scan.framework.AggregateResultListener\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.ResultAggregator\nimport com.getbouncer.scan.framework.time.Rate\nimport com.getbouncer.scan.framework.util.FrameSaver\nimport com.getbouncer.scan.payment.FrameDetails\nimport com.getbouncer.scan.payment.card.isValidPan\nimport kotlinx.coroutines.runBlocking\n\nprivate const val MAX_SAVED_FRAMES_PER_TYPE = 6\n\n/**\n * Aggregate results from the main loop. Each frame will trigger an [InterimResult] to the [listener]. Once the\n * [MainLoopState.Finished] state is reached, a [FinalResult] will be sent to the [listener].\n *\n * This aggregator is a state machine. The full list of possible states are subclasses of [MainLoopState]. This was\n * written referencing this article: https://thoughtbot.com/blog/finite-state-machines-android-kotlin-good-times\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass MainLoopAggregator(\n    listener: AggregateResultListener<InterimResult, FinalResult>,\n) : ResultAggregator<MainLoopAnalyzer.Input, MainLoopState, MainLoopAnalyzer.Prediction, MainLoopAggregator.InterimResult, MainLoopAggregator.FinalResult>(\n    listener = listener,\n    initialState = MainLoopState.Initial()\n) {\n\n    @Keep\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    data class FinalResult(\n        val pan: String,\n        val savedFrames: Map<SavedFrameType, List<SavedFrame>>,\n        val averageFrameRate: Rate,\n    )\n\n    @Keep\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    data class InterimResult(\n        val analyzerResult: MainLoopAnalyzer.Prediction,\n        val frame: MainLoopAnalyzer.Input,\n        val state: MainLoopState,\n    )\n\n    private val frameSaver = object : FrameSaver<SavedFrameType, SavedFrame, InterimResult>() {\n        override fun getMaxSavedFrames(savedFrameIdentifier: SavedFrameType): Int =\n            MAX_SAVED_FRAMES_PER_TYPE\n        override fun getSaveFrameIdentifier(frame: SavedFrame, metaData: InterimResult): SavedFrameType? {\n            val hasCard = metaData.analyzerResult.isCardVisible == true\n            val hasPan = isValidPan(metaData.analyzerResult.ocr?.pan)\n            return if (hasCard || hasPan) SavedFrameType(hasCard = hasCard, hasPan = hasPan) else null\n        }\n    }\n\n    override suspend fun aggregateResult(\n        frame: MainLoopAnalyzer.Input,\n        result: MainLoopAnalyzer.Prediction\n    ): Pair<InterimResult, FinalResult?> {\n        val previousState = state\n        val currentState = previousState.consumeTransition(result)\n\n        state = currentState\n\n        val interimResult = InterimResult(\n            analyzerResult = result,\n            frame = frame,\n            state = currentState,\n        )\n\n        val mostLikelyPan = when (currentState) {\n            is MainLoopState.Initial -> null\n            is MainLoopState.PanFound -> currentState.getMostLikelyPan()\n            is MainLoopState.PanSatisfied -> currentState.pan\n            is MainLoopState.CardSatisfied -> currentState.getMostLikelyPan()\n            is MainLoopState.Finished -> currentState.pan\n        }\n\n        val savedFrame = SavedFrame(\n            pan = result.ocr?.pan ?: mostLikelyPan,\n            frame = frame,\n            details = FrameDetails(\n                hasPan = isValidPan(result.ocr?.pan),\n                panSideConfidence = result.card?.panProbability ?: 0F,\n                noPanSideConfidence = result.card?.noPanProbability ?: 0F,\n                noCardConfidence = result.card?.noCardProbability ?: 0F,\n            ),\n        )\n\n        frame.cameraPreviewImage.image.tracker.trackResult(\"main_loop_aggregated\")\n        if (Config.isDebug) {\n            Log.d(Config.logTag, \"Delay between capture and process of image is ${frame.cameraPreviewImage.image.tracker.startedAt.elapsedSince()}\")\n        }\n\n        frameSaver.saveFrame(savedFrame, interimResult)\n\n        return if (currentState is MainLoopState.Finished) {\n            val savedFrames = frameSaver.getSavedFrames()\n            frameSaver.reset()\n            interimResult to FinalResult(\n                pan = currentState.pan,\n                savedFrames = savedFrames,\n                averageFrameRate = frameRateTracker.getAverageFrameRate(),\n            )\n        } else {\n            interimResult to null\n        }\n    }\n\n    override fun reset() {\n        super.reset()\n        runBlocking { frameSaver.reset() }\n    }\n}\n"
  },
  {
    "path": "cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/result/MainLoopStateMachine.kt",
    "content": "package com.getbouncer.cardscan.ui.result\n\nimport androidx.annotation.VisibleForTesting\nimport com.getbouncer.cardscan.ui.analyzer.MainLoopAnalyzer\nimport com.getbouncer.scan.framework.MachineState\nimport com.getbouncer.scan.framework.time.seconds\nimport com.getbouncer.scan.framework.util.ItemTotalCounter\nimport com.getbouncer.scan.payment.card.isValidPan\n\n@VisibleForTesting\ninternal val PAN_SEARCH_DURATION = 5.seconds\n\n@VisibleForTesting\ninternal val PAN_AND_CARD_SEARCH_DURATION = 10.seconds\n\n@VisibleForTesting\ninternal val DESIRED_PAN_AGREEMENT = 5\n\n@VisibleForTesting\ninternal val MINIMUM_PAN_AGREEMENT = 2\n\n@VisibleForTesting\ninternal val DESIRED_SIDE_COUNT = 8\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nsealed class MainLoopState(\n    val runOcr: Boolean,\n    val runCardDetect: Boolean,\n) : MachineState() {\n\n    internal abstract suspend fun consumeTransition(\n        transition: MainLoopAnalyzer.Prediction,\n    ): MainLoopState\n\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    class Initial : MainLoopState(runOcr = true, runCardDetect = false) {\n        override suspend fun consumeTransition(\n            transition: MainLoopAnalyzer.Prediction,\n        ): MainLoopState = when {\n            isValidPan(transition.ocr?.pan) ->\n                PanFound(ItemTotalCounter(transition.ocr?.pan ?: \"\"))\n            else -> this\n        }\n    }\n\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    class PanFound(\n        private val panCounter: ItemTotalCounter<String>,\n    ) : MainLoopState(runOcr = true, runCardDetect = true) {\n        private var visibleCardCount = 0\n\n        fun getMostLikelyPan() = panCounter.getHighestCountItem()?.second\n\n        private fun isCardSatisfied() = visibleCardCount >= DESIRED_SIDE_COUNT\n        private fun isPanSatisfied() =\n            (panCounter.getHighestCountItem()?.first ?: 0) >= DESIRED_PAN_AGREEMENT ||\n                (\n                    (\n                        panCounter.getHighestCountItem()?.first\n                            ?: 0\n                        ) >= MINIMUM_PAN_AGREEMENT &&\n                        reachedStateAt.elapsedSince() > PAN_SEARCH_DURATION\n                    )\n\n        override suspend fun consumeTransition(\n            transition: MainLoopAnalyzer.Prediction,\n        ): MainLoopState {\n            if (isValidPan(transition.ocr?.pan)) {\n                panCounter.countItem(transition.ocr?.pan ?: \"\")\n            }\n\n            if (transition.isCardVisible == true) {\n                visibleCardCount++\n            }\n\n            return when {\n                reachedStateAt.elapsedSince() > PAN_AND_CARD_SEARCH_DURATION -> Finished(getMostLikelyPan() ?: \"\")\n                isCardSatisfied() && isPanSatisfied() -> Finished(getMostLikelyPan() ?: \"\")\n                isCardSatisfied() -> CardSatisfied(panCounter)\n                isPanSatisfied() -> PanSatisfied(getMostLikelyPan() ?: \"\", visibleCardCount)\n                else -> this\n            }\n        }\n    }\n\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    class PanSatisfied(\n        val pan: String,\n        var visibleCardCount: Int,\n    ) : MainLoopState(runOcr = false, runCardDetect = true) {\n        private fun isCardSatisfied() = visibleCardCount >= DESIRED_SIDE_COUNT\n\n        override suspend fun consumeTransition(\n            transition: MainLoopAnalyzer.Prediction,\n        ): MainLoopState {\n            if (transition.isCardVisible == true) {\n                visibleCardCount++\n            }\n\n            return when {\n                reachedStateAt.elapsedSince() > PAN_SEARCH_DURATION -> Finished(pan)\n                isCardSatisfied() -> Finished(pan)\n                else -> this\n            }\n        }\n    }\n\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    class CardSatisfied(\n        private val panCounter: ItemTotalCounter<String>,\n    ) : MainLoopState(runOcr = true, runCardDetect = false) {\n        fun getMostLikelyPan() = panCounter.getHighestCountItem()?.second\n        private fun isPanSatisfied() =\n            panCounter.getHighestCountItem()?.first ?: 0 >= DESIRED_PAN_AGREEMENT\n\n        override suspend fun consumeTransition(\n            transition: MainLoopAnalyzer.Prediction,\n        ): MainLoopState {\n            if (transition.ocr?.pan != null && isValidPan(transition.ocr.pan)) {\n                panCounter.countItem(transition.ocr.pan)\n            }\n\n            return when {\n                isPanSatisfied() -> Finished(getMostLikelyPan() ?: \"\")\n                reachedStateAt.elapsedSince() >= PAN_SEARCH_DURATION ->\n                    Finished(getMostLikelyPan() ?: \"\")\n                else -> this\n            }\n        }\n    }\n\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    class Finished(val pan: String) : MainLoopState(runOcr = false, runCardDetect = false) {\n        override suspend fun consumeTransition(\n            transition: MainLoopAnalyzer.Prediction,\n        ): MainLoopState = this\n    }\n}\n"
  },
  {
    "path": "cardscan-ui/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"bouncerProcessingBackground\">#AA000000</color>\n    <color name=\"bouncerProcessingText\">@android:color/white</color>\n\n    <color name=\"bouncerEnterCardManuallyColorDark\" description=\"The color of the Enter Card Manually text displayed at the bottom of the screen when the background is a dark color\">@android:color/white</color>\n    <color name=\"bouncerEnterCardManuallyColorLight\" description=\"The color of the Enter Card Manually text displayed at the bottom of the screen when the background is a light color\">@android:color/black</color>\n</resources>\n"
  },
  {
    "path": "cardscan-ui/src/main/res/values/dimensions.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <dimen name=\"bouncerProcessingTextSize\">24sp</dimen>\n\n    <dimen name=\"bouncerButtonMargin\">24dp</dimen>\n\n    <dimen name=\"bouncerEnterCardManuallyMargin\" description=\"The minimum amount of space surrounding the Enter Card Mandually text at the bottom of the screen\">16dp</dimen>\n    <dimen name=\"bouncerEnterCardManuallyTextSize\" description=\"The size of the Enter Card Manually text at the bottom of the screen\">18sp</dimen>\n</resources>\n"
  },
  {
    "path": "cardscan-ui/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"bouncer_processing_card\">Processing, please wait</string>\n\n    <string name=\"bouncer_enter_card_manually\" description=\"Text shown at the bottom of the screen offering for the user to type in card details manually\">Enter card manually</string>\n</resources>\n"
  },
  {
    "path": "cardscan-ui/src/test/java/com/getbouncer/cardscan/ui/result/MainLoopStateMachineTest.kt",
    "content": "package com.getbouncer.cardscan.ui.result\n\nimport androidx.test.filters.LargeTest\nimport com.getbouncer.cardscan.ui.analyzer.MainLoopAnalyzer\nimport com.getbouncer.scan.framework.time.delay\nimport com.getbouncer.scan.framework.time.milliseconds\nimport com.getbouncer.scan.framework.util.ItemTotalCounter\nimport com.getbouncer.scan.payment.ml.CardDetect\nimport com.getbouncer.scan.payment.ml.SSDOcr\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.test.runBlockingTest\nimport org.junit.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFalse\nimport kotlin.test.assertSame\nimport kotlin.test.assertTrue\n\nclass MainLoopStateMachineTest {\n\n    @Test\n    fun initial_runsOcrOnly() {\n        val state = MainLoopState.Initial()\n\n        assertTrue(state.runOcr)\n        assertFalse(state.runCardDetect)\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun initial_noCard_noOcr() = runBlockingTest {\n        val state = MainLoopState.Initial()\n\n        val prediction = MainLoopAnalyzer.Prediction(\n            ocr = null,\n            card = null,\n        )\n\n        val newState = state.consumeTransition(prediction)\n        assertTrue(newState is MainLoopState.Initial)\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun initial_noCard_foundOcr() = runBlockingTest {\n        val state = MainLoopState.Initial()\n\n        val prediction = MainLoopAnalyzer.Prediction(\n            ocr = SSDOcr.Prediction(\n                pan = \"4847186095118770\",\n                detectedBoxes = emptyList(),\n            ),\n            card = null,\n        )\n\n        val newState = state.consumeTransition(prediction)\n        assertTrue(newState is MainLoopState.PanFound)\n        assertEquals(\"4847186095118770\", newState.getMostLikelyPan())\n    }\n\n    @Test\n    fun panFound_runsCardDetectAndOcrOnly() {\n        val state = MainLoopState.PanFound(\n            panCounter = ItemTotalCounter(\"4847186095118770\"),\n        )\n\n        assertTrue(state.runOcr)\n        assertTrue(state.runCardDetect)\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun panFound_noCard_noTimeout() = runBlockingTest {\n        val state = MainLoopState.PanFound(\n            panCounter = ItemTotalCounter(\"4847186095118770\"),\n        )\n\n        val prediction = MainLoopAnalyzer.Prediction(\n            ocr = SSDOcr.Prediction(\n                pan = \"4847186095118770\",\n                detectedBoxes = emptyList(),\n            ),\n            card = null,\n        )\n\n        val newState = state.consumeTransition(prediction)\n        assertTrue(newState is MainLoopState.PanFound)\n        assertEquals(\"4847186095118770\", newState.getMostLikelyPan())\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun panFound_cardSatisfied_noTimeout() = runBlockingTest {\n        var state: MainLoopState = MainLoopState.PanFound(\n            panCounter = ItemTotalCounter(\"4847186095118770\"),\n        )\n\n        val prediction = MainLoopAnalyzer.Prediction(\n            ocr = null,\n            card = CardDetect.Prediction(\n                side = CardDetect.Prediction.Side.PAN,\n                panProbability = 1.0F,\n                noPanProbability = 0.0F,\n                noCardProbability = 0.0F,\n            ),\n        )\n\n        repeat(DESIRED_SIDE_COUNT - 1) {\n            state = state.consumeTransition(prediction)\n            assertTrue(state is MainLoopState.PanFound)\n        }\n\n        val newState = state.consumeTransition(prediction)\n        assertTrue(newState is MainLoopState.CardSatisfied)\n        assertEquals(\"4847186095118770\", newState.getMostLikelyPan())\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun panFound_panSatisfied_noTimeout() = runBlockingTest {\n        var state: MainLoopState = MainLoopState.PanFound(\n            panCounter = ItemTotalCounter(\"4847186095118770\"),\n        )\n\n        val prediction = MainLoopAnalyzer.Prediction(\n            ocr = SSDOcr.Prediction(\n                pan = \"4847186095118770\",\n                detectedBoxes = emptyList(),\n            ),\n            card = null,\n        )\n\n        repeat(DESIRED_PAN_AGREEMENT - 2) {\n            state = state.consumeTransition(prediction)\n            assertTrue(state is MainLoopState.PanFound)\n        }\n\n        val newState = state.consumeTransition(prediction)\n        assertTrue(newState is MainLoopState.PanSatisfied)\n        assertEquals(\"4847186095118770\", newState.pan)\n    }\n\n    /**\n     * This test cannot use `runBlockingTest` because it requires a delay. While runBlockingTest\n     * advances the dispatcher's virtual time by the specified amount, it does not affect the timing\n     * of the duration.\n     */\n    @Test\n    @ExperimentalCoroutinesApi\n    fun panFound_panSatisfied_timeout() = runBlocking {\n        var state: MainLoopState = MainLoopState.PanFound(\n            panCounter = ItemTotalCounter(\"4847186095118770\"),\n        )\n\n        val prediction = MainLoopAnalyzer.Prediction(\n            ocr = SSDOcr.Prediction(\n                pan = \"4847186095118770\",\n                detectedBoxes = emptyList(),\n            ),\n            card = null,\n        )\n\n        repeat(MINIMUM_PAN_AGREEMENT - 2) {\n            state = state.consumeTransition(prediction)\n            assertTrue(state is MainLoopState.PanFound)\n        }\n\n        delay(PAN_SEARCH_DURATION + 1.milliseconds)\n\n        val newState = state.consumeTransition(prediction)\n        assertTrue(newState is MainLoopState.PanSatisfied)\n        assertEquals(\"4847186095118770\", newState.pan)\n    }\n\n    /**\n     * This test cannot use `runBlockingTest` because it requires a delay. While runBlockingTest\n     * advances the dispatcher's virtual time by the specified amount, it does not affect the timing\n     * of the duration.\n     */\n    @Test\n    @LargeTest\n    fun panFound_timeout() = runBlocking {\n        val state = MainLoopState.PanFound(\n            panCounter = ItemTotalCounter(\"4847186095118770\"),\n        )\n\n        delay(PAN_AND_CARD_SEARCH_DURATION + 1.milliseconds)\n\n        val prediction = MainLoopAnalyzer.Prediction(\n            ocr = null,\n            card = null,\n        )\n\n        val newState = state.consumeTransition(prediction)\n        assertTrue(newState is MainLoopState.Finished, \"$newState is not Finished\")\n        assertEquals(\"4847186095118770\", newState.pan)\n    }\n\n    @Test\n    fun panSatisfied_runsCardDetectOnly() {\n        val state = MainLoopState.PanSatisfied(\n            pan = \"4847186095118770\",\n            visibleCardCount = 0,\n        )\n\n        assertFalse(state.runOcr)\n        assertTrue(state.runCardDetect)\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun panSatisfied_noCard_noTimeout() = runBlockingTest {\n        val state = MainLoopState.PanSatisfied(\n            pan = \"4847186095118770\",\n            visibleCardCount = 0,\n        )\n\n        val prediction = MainLoopAnalyzer.Prediction(\n            ocr = null,\n            card = CardDetect.Prediction(\n                side = CardDetect.Prediction.Side.PAN,\n                panProbability = 1.0F,\n                noPanProbability = 0.0F,\n                noCardProbability = 0.0F,\n            ),\n        )\n\n        val newState = state.consumeTransition(prediction)\n        assertTrue(newState is MainLoopState.PanSatisfied)\n        assertEquals(\"4847186095118770\", newState.pan)\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun panSatisfied_enoughSides_noTimeout() = runBlockingTest {\n        val state = MainLoopState.PanSatisfied(\n            pan = \"4847186095118770\",\n            visibleCardCount = DESIRED_SIDE_COUNT - 1,\n        )\n\n        val prediction = MainLoopAnalyzer.Prediction(\n            ocr = null,\n            card = CardDetect.Prediction(\n                side = CardDetect.Prediction.Side.PAN,\n                panProbability = 1.0F,\n                noPanProbability = 0.0F,\n                noCardProbability = 0.0F,\n            ),\n        )\n\n        val newState = state.consumeTransition(prediction)\n        assertTrue(newState is MainLoopState.Finished)\n        assertEquals(\"4847186095118770\", newState.pan)\n    }\n\n    /**\n     * This test cannot use `runBlockingTest` because it requires a delay. While runBlockingTest\n     * advances the dispatcher's virtual time by the specified amount, it does not affect the timing\n     * of the duration.\n     */\n    @Test\n    @LargeTest\n    fun panSatisfied_timeout() = runBlocking {\n        val state = MainLoopState.PanSatisfied(\n            pan = \"4847186095118770\",\n            visibleCardCount = DESIRED_SIDE_COUNT - 1,\n        )\n\n        val prediction = MainLoopAnalyzer.Prediction(\n            ocr = null,\n            card = null,\n        )\n\n        delay(PAN_SEARCH_DURATION + 1.milliseconds)\n\n        val newState = state.consumeTransition(prediction)\n        assertTrue(newState is MainLoopState.Finished)\n        assertEquals(\"4847186095118770\", newState.pan)\n    }\n\n    @Test\n    fun cardSatisfied_runsOcrOnly() {\n        val state = MainLoopState.CardSatisfied(\n            panCounter = ItemTotalCounter(\"4847186095118770\"),\n        )\n\n        assertTrue(state.runOcr)\n        assertFalse(state.runCardDetect)\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun cardSatisfied_noPan_noTimeout() = runBlockingTest {\n        val state = MainLoopState.CardSatisfied(\n            panCounter = ItemTotalCounter(\"4847186095118770\"),\n        )\n\n        val prediction = MainLoopAnalyzer.Prediction(\n            ocr = SSDOcr.Prediction(\n                pan = \"4847186095118770\",\n                detectedBoxes = emptyList(),\n            ),\n            card = null,\n        )\n\n        val newState = state.consumeTransition(prediction)\n        assertTrue(newState is MainLoopState.CardSatisfied)\n        assertEquals(\"4847186095118770\", newState.getMostLikelyPan())\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun cardSatisfied_pan_noTimeout() = runBlockingTest {\n        var state: MainLoopState = MainLoopState.CardSatisfied(\n            panCounter = ItemTotalCounter(\"4847186095118770\"),\n        )\n\n        val prediction = MainLoopAnalyzer.Prediction(\n            ocr = SSDOcr.Prediction(\n                pan = \"4847186095118770\",\n                detectedBoxes = emptyList(),\n            ),\n            card = null,\n        )\n\n        repeat(DESIRED_PAN_AGREEMENT - 2) {\n            state = state.consumeTransition(prediction)\n            assertTrue(state is MainLoopState.CardSatisfied)\n        }\n\n        val newState = state.consumeTransition(prediction)\n        assertTrue(newState is MainLoopState.Finished)\n        assertEquals(\"4847186095118770\", newState.pan)\n    }\n\n    /**\n     * This test cannot use `runBlockingTest` because it requires a delay. While runBlockingTest\n     * advances the dispatcher's virtual time by the specified amount, it does not affect the timing\n     * of the duration.\n     */\n    @Test\n    @LargeTest\n    fun cardSatisfied_noPan_timeout() = runBlocking {\n        val state = MainLoopState.CardSatisfied(\n            panCounter = ItemTotalCounter(\"4847186095118770\"),\n        )\n\n        val prediction = MainLoopAnalyzer.Prediction(\n            ocr = null,\n            card = null,\n        )\n\n        delay(PAN_SEARCH_DURATION + 1.milliseconds)\n\n        val newState = state.consumeTransition(prediction)\n        assertTrue(newState is MainLoopState.Finished)\n        assertEquals(\"4847186095118770\", newState.pan)\n    }\n\n    @Test\n    fun finished_runsNothing() {\n        val state = MainLoopState.Finished(\n            pan = \"4847186095118770\",\n        )\n\n        assertFalse(state.runOcr)\n        assertFalse(state.runCardDetect)\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun finished_goesNowhere() = runBlockingTest {\n        val state = MainLoopState.Finished(\n            pan = \"4847186095118770\",\n        )\n\n        val prediction = MainLoopAnalyzer.Prediction(\n            ocr = SSDOcr.Prediction(\n                pan = \"4847186095118770\",\n                detectedBoxes = emptyList(),\n            ),\n            card = CardDetect.Prediction(\n                side = CardDetect.Prediction.Side.NO_CARD,\n                panProbability = 0.0F,\n                noPanProbability = 0.0F,\n                noCardProbability = 1.0F,\n            ),\n        )\n\n        val newState = state.consumeTransition(prediction)\n        assertSame(state, newState)\n    }\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Fri Feb 26 15:43:08 PST 2021\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.9.1-all.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx1536m\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app's APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n# Automatically convert third-party libraries to use AndroidX\nandroid.enableJetifier=true\n# Kotlin code style for this project: \"official\" or \"obsolete\":\nkotlin.code.style=official\nversion=2.2.0003\n"
  },
  {
    "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\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windows variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\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%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "scan-camera/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "scan-camera/README.md",
    "content": "# Deprecation Notice\nHello from the Stripe (formerly Bouncer) team!\n\nWe're excited to provide an update on the state and future of the [Card Scan OCR](https://github.com/stripe/stripe-android/tree/master/stripecardscan) product! As we continue to build into Stripe's ecosystem, we'll be supporting the mission to continuously improve the end customer experience in many of Stripe's core checkout products.\n\nThis SDK has been [migrated to Stripe](https://github.com/stripe/stripe-android/tree/master/stripecardscan) and is now free for use under the MIT license!\n\nIf you are not currently a Stripe user, and interested in learning more about improving checkout experience through Stripe, please let us know and we can connect you with the team.\n\nIf you are not currently a Stripe user, and want to continue using the existing SDK, you can do so free of charge. Starting January 1, 2022, we will no longer be charging for use of the existing Bouncer Card Scan OCR SDK. For product support on [Android](https://github.com/stripe/stripe-android/issues) and [iOS](https://github.com/stripe/stripe-ios/issues). For billing support, please email [bouncer-support@stripe.com](mailto:bouncer-support@stripe.com).\n\nFor the new product, please visit the [stripe github repository](https://github.com/stripe/stripe-android/tree/master/stripecardscan).\n\n# Overview\nThis repository contains the legacy, deprecated open source camera framework to allow scanning cards. [CardScan](https://cardscan.io/) is a relatively small library that provides fast and accurate payment card scanning.\n\nNote this library does not contain any user interfaces. Another library, [CardScan UI](https://github.com/getbouncer/cardscan-ui-android) builds upon this one any adds simple user interfaces. \n\n![demo](../docs/images/demo.gif)\n\n## Contents\n* [Requirements](#requirements)\n* [Demo](#demo)\n* [Integration](#integration)\n* [Using](#using)\n* [Developing](#developing)\n* [Authors](#authors)\n* [License](#license)\n\n## Requirements\n* Android API level 21 or higher\n* Kotlin coroutine compatibility\n\nNote: Your app does not have to be written in kotlin to integrate scan-camera, but must be able to depend on kotlin functionality.\n\n## Demo\nAn app demonstrating the basic capabilities of CardScan is available in [github](https://github.com/getbouncer/cardscan-demo-android).\n\n## Integration\nSee the [integration documentation](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) in the Bouncer Docs.\n\n## Using\nThis library is designed to be used with [CardScan UI](https://github.com/getbouncer/cardscan-ui-android), which will provide user interfaces for scanning payment cards. However, it can be used independently.\n\nFor an overview of the architecture and design of the scan framework, see the [architecture documentation](https://docs.getbouncer.com/card-scan/android-integration-guide/android-architecture-overview).\n\n### Getting images from the camera\nSee the [example code](https://docs.getbouncer.com/card-scan/android-integration-guide/android-architecture-overview#example) in the Android architecture documentation.\n\n## Developing\nSee the [development docs](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) for details on developing this library.\n\n## Authors\nAdam Wushensky, Sam King, and Zain ul Abi Din\n\n## License\nThis library is available under the MIT license. See the [LICENSE](../LICENSE) file for the full license text.\n"
  },
  {
    "path": "scan-camera/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\n\nandroid {\n    compileSdkVersion 30\n    buildToolsVersion '30.0.3'\n\n    defaultConfig {\n        minSdkVersion 21\n        targetSdkVersion 30\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles 'consumer-rules.pro'\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    testOptions {\n        unitTests.includeAndroidResources = true\n    }\n\n    lintOptions {\n        enable \"Interoperability\"\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation project(\":scan-framework\")\n\n    implementation \"androidx.appcompat:appcompat:[1.3.0,1.3.1]\"\n    implementation \"androidx.core:core-ktx:[1.3.1,1.6.0]\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-core:[1.4.0,1.5.1]\"\n}\n\ndependencies {\n    testImplementation \"androidx.test:core:1.4.0\"\n    testImplementation \"androidx.test:runner:1.4.0\"\n    testImplementation \"junit:junit:4.13.2\"\n    testImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n    testImplementation \"org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1\"\n}\n\ndependencies {\n    androidTestImplementation \"androidx.test.ext:junit:1.1.3\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.4.0\"\n    androidTestImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n}\n\napply from: 'deploy.gradle'\n"
  },
  {
    "path": "scan-camera/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "scan-camera/deploy.gradle",
    "content": "apply plugin: 'maven-publish'\napply plugin: 'org.jetbrains.dokka'\napply plugin: 'signing'\n\ntask androidSourcesJar(type: Jar) {\n    archiveClassifier.set('sources')\n    if (project.plugins.findPlugin(\"com.android.library\")) {\n        // Android library\n        from android.sourceSets.main.java.srcDirs\n        from android.sourceSets.main.kotlin.srcDirs\n    } else {\n        // Pure kotlin library\n        from sourceSets.main.java.srcDirs\n        from sourceSets.main.kotlin.srcDirs\n    }\n}\n\ntasks.withType(dokkaHtmlPartial.getClass()).configureEach {\n    pluginsMapConfiguration.set(\n            [\"org.jetbrains.dokka.base.DokkaBase\": \"\"\"{ \"separateInheritedMembers\": true}\"\"\"]\n    )\n}\n\ntask javadocJar(type: Jar, dependsOn: dokkaJavadoc) {\n    archiveClassifier.set('javadoc')\n    from dokkaJavadoc.outputDirectory\n}\n\nartifacts {\n    archives androidSourcesJar\n    archives javadocJar\n}\n\next[\"signing.keyId\"] = ''\next[\"signing.password\"] = ''\next[\"signing.secretKeyRingFile\"] = ''\n\next[\"ossrhUsername\"] = ''\next[\"ossrhPassword\"] = ''\next[\"sonatypeStagingProfileId\"] = ''\n\next {\n\n    libraryDescription = 'This library provides the framework for cameras'\n\n    siteUrl = 'https://getbouncer.com'\n\n    scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'\n    scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'\n    scmUrl = 'https://github.com/getbouncer/cardscan-android'\n\n    licenseName = 'bouncer-free-1'\n    licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'\n\n    developerId = 'getbouncer'\n    developerName = 'Bouncer Technologies'\n    developerEmail = 'bouncer-support@stripe.com'\n\n    publishGroupId = 'com.getbouncer'\n    publishArtifactId = 'scan-camera'\n    publishVersion = version\n}\n\ngroup = publishGroupId\nversion = publishVersion\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    Properties p = new Properties()\n    new FileInputStream(secretPropsFile).withCloseable { is ->\n        p.load(is)\n    }\n    p.each { name, value ->\n        ext[name] = value\n    }\n} else {\n    ext[\"signing.keyId\"] = System.getenv('SIGNING_KEY_ID')\n    ext[\"signing.password\"] = System.getenv('SIGNING_PASSWORD')\n    ext[\"signing.secretKeyRingFile\"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')\n\n    ext[\"ossrhUsername\"] = System.getenv('OSSRH_USERNAME')\n    ext[\"ossrhPassword\"] = System.getenv('OSSRH_PASSWORD')\n\n    ext[\"sonatypeStagingProfileId\"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')\n}\n\npublishing {\n    publications {\n        release(MavenPublication) {\n            groupId publishGroupId\n            artifactId publishArtifactId\n            version publishVersion\n\n            // Two artifacts, the `aar` (or `jar`) and the sources\n            if (project.plugins.findPlugin(\"com.android.library\")) {\n                artifact(\"$buildDir/outputs/aar/${project.getName()}-release.aar\")\n            } else {\n                artifact(\"$buildDir/libs/${project.getName()}-${version}.jar\")\n            }\n            artifact androidSourcesJar\n\n            pom {\n                name = publishArtifactId\n                description = libraryDescription\n                url = siteUrl\n                licenses {\n                    license {\n                        name = licenseName\n                        url = licenseUrl\n                    }\n                }\n                developers {\n                    developer {\n                        id = developerId\n                        name = developerName\n                        email = developerEmail\n                    }\n                }\n                scm {\n                    connection = scmConnection\n                    developerConnection = scmDeveloperConnection\n                    url = scmUrl\n                }\n                // A slightly hacky fix so that your POM will include any transitive dependencies\n                // that your library builds upon\n                withXml {\n                    def dependenciesNode = asNode().appendNode('dependencies')\n\n                    project.configurations.implementation.allDependencies.each {\n                        if (it.group != null && it.version != null) {\n                            def dependencyNode = dependenciesNode.appendNode('dependency')\n                            dependencyNode.appendNode('groupId', it.group)\n                            dependencyNode.appendNode('artifactId', it.name)\n                            dependencyNode.appendNode('version', it.version)\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // The repository to publish to, Sonatype/MavenCentral\n    repositories {\n        maven {\n            // This is an arbitrary name, you may also use \"mavencentral\" or\n            // any other name that's descriptive for you\n            name = \"sonatype\"\n            url = \"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/\"\n            credentials {\n                username ossrhUsername\n                password ossrhPassword\n            }\n        }\n    }\n}\n\nsigning {\n    sign publishing.publications\n}\n"
  },
  {
    "path": "scan-camera/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": "scan-camera/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.getbouncer.scan.camera\" >\n\n    <uses-permission android:name=\"android.permission.CAMERA\" />\n    <uses-permission android:name=\"android.permission.FLASHLIGHT\" />\n\n    <uses-feature android:name=\"android.hardware.camera\" android:required=\"false\" />\n    <uses-feature android:name=\"android.hardware.camera.autofocus\" android:required=\"false\" />\n\n</manifest>\n"
  },
  {
    "path": "scan-camera/src/main/java/com/getbouncer/scan/camera/Camera1Adapter.kt",
    "content": "@file:Suppress(\"deprecation\")\npackage com.getbouncer.scan.camera\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.ImageFormat\nimport android.graphics.PointF\nimport android.graphics.Rect\nimport android.hardware.Camera\nimport android.hardware.Camera.AutoFocusCallback\nimport android.hardware.Camera.PreviewCallback\nimport android.os.Handler\nimport android.os.HandlerThread\nimport android.util.DisplayMetrics\nimport android.util.Log\nimport android.util.Size\nimport android.view.SurfaceHolder\nimport android.view.SurfaceView\nimport android.view.ViewGroup\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.OnLifecycleEvent\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.Stats\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.image.NV21Image\nimport com.getbouncer.scan.framework.image.getRenderScript\nimport com.getbouncer.scan.framework.image.rotate\nimport com.getbouncer.scan.framework.util.retrySync\nimport java.lang.ref.WeakReference\nimport java.util.ArrayList\nimport kotlin.math.abs\nimport kotlin.math.max\nimport kotlin.math.min\nimport kotlin.math.roundToInt\n\nprivate const val ASPECT_TOLERANCE = 0.2\n\nprivate val MAXIMUM_RESOLUTION = Size(1920, 1080)\n\n/**\n * A [CameraAdapter] that uses android's Camera 1 APIs to show previews and process images.\n */\ninternal class Camera1Adapter(\n    private val activity: Activity,\n    private val previewView: ViewGroup,\n    private val minimumResolution: Size,\n    private val cameraErrorListener: CameraErrorListener,\n) : CameraAdapter<CameraPreviewImage<Bitmap>>(), PreviewCallback {\n    override val implementationName: String = \"Camera1\"\n\n    private var mCamera: Camera? = null\n    private var cameraPreview: CameraPreview? = null\n    private var mRotation = 0\n    private var onCameraAvailableListener: WeakReference<((Camera) -> Unit)?> = WeakReference(null)\n    private var currentCameraId = 0\n\n    private val mainThreadHandler = Handler(activity.mainLooper)\n    private var cameraThread: HandlerThread? = null\n    private var cameraHandler: Handler? = null\n\n    override fun withFlashSupport(task: (Boolean) -> Unit) {\n        mCamera?.let {\n            task(isFlashSupported(it))\n        } ?: run {\n            onCameraAvailableListener = WeakReference { cam ->\n                task(isFlashSupported(cam))\n            }\n        }\n    }\n\n    private fun isFlashSupported(camera: Camera) =\n        camera.parameters?.supportedFlashModes?.contains(Camera.Parameters.FLASH_MODE_TORCH) == true\n\n    override fun setTorchState(on: Boolean) {\n        mCamera?.apply {\n            val parameters = parameters\n            if (on) {\n                parameters.flashMode = Camera.Parameters.FLASH_MODE_TORCH\n            } else {\n                parameters.flashMode = Camera.Parameters.FLASH_MODE_OFF\n            }\n            setCameraParameters(this, parameters)\n            startCameraPreview()\n        }\n    }\n\n    override fun isTorchOn(): Boolean =\n        mCamera?.parameters?.flashMode == Camera.Parameters.FLASH_MODE_TORCH\n\n    override fun setFocus(point: PointF) {\n        mCamera?.apply {\n            val params = parameters\n            if (params.maxNumFocusAreas > 0) {\n                val focusRect = Rect(\n                    point.x.toInt() - 150,\n                    point.y.toInt() - 150,\n                    point.x.toInt() + 150,\n                    point.y.toInt() + 150\n                )\n                val cameraFocusAreas: MutableList<Camera.Area> = ArrayList()\n                cameraFocusAreas.add(Camera.Area(focusRect, 1000))\n                params.focusAreas = cameraFocusAreas\n                setCameraParameters(this, params)\n            }\n        }\n    }\n\n    override fun onPreviewFrame(bytes: ByteArray?, camera: Camera) {\n        // this method may be called after the camera has closed if there was still an image in\n        // flight. In this case, swallow the error. Ideally, we would be able to tell whether the\n        // exception was due to the camera already having been closed or from an error with camera\n        // hardware.\n        val imageWidth = try { camera.parameters.previewSize.width } catch (t: Throwable) { return }\n        val imageHeight = try { camera.parameters.previewSize.height } catch (t: Throwable) { return }\n\n        if (bytes != null) {\n            try {\n                sendImageToStream(\n                    CameraPreviewImage(\n                        TrackedImage(\n                            image = NV21Image(imageWidth, imageHeight, bytes)\n                                .toBitmap(getRenderScript(activity))\n                                .rotate(mRotation.toFloat()),\n                            tracker = Stats.trackRepeatingTask(\"image_processing\")\n                        ),\n                        Rect(0, 0, previewView.width, previewView.height),\n                    ),\n                )\n            } catch (t: Throwable) {\n                // ignore errors transforming the image (OOM, etc)\n                Log.e(Config.logTag, \"Exception caught during camera transform\", t)\n            } finally {\n                camera.addCallbackBuffer(bytes)\n            }\n        } else {\n            camera.addCallbackBuffer(ByteArray((imageWidth * imageHeight * 1.5).roundToInt()))\n        }\n    }\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)\n    override fun onPause() {\n        super.onPause()\n\n        mCamera?.stopPreview()\n        mCamera?.setPreviewCallbackWithBuffer(null)\n        mCamera?.release()\n        mCamera = null\n\n        cameraPreview?.apply { holder.removeCallback(this) }\n        cameraPreview = null\n\n        stopCameraThread()\n    }\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)\n    fun onResume() {\n        startCameraThread()\n\n        mainThreadHandler.post {\n            try {\n                var camera: Camera? = null\n                try {\n                    camera = Camera.open(currentCameraId)\n                } catch (t: Throwable) {\n                    cameraErrorListener.onCameraOpenError(t)\n                }\n                onCameraOpen(camera)\n            } catch (t: Throwable) {\n                cameraErrorListener.onCameraOpenError(t)\n            }\n        }\n    }\n\n    /**\n     * Starts a background thread and its [Handler].\n     */\n    private fun startCameraThread() {\n        val thread = HandlerThread(\"CameraBackground\").also { it.start() }\n        cameraThread = thread\n        cameraHandler = Handler(thread.looper)\n    }\n\n    /**\n     * Stops the background thread and its [Handler].\n     */\n    private fun stopCameraThread() {\n        cameraThread?.quitSafely()\n        try {\n            cameraThread?.join()\n            cameraThread = null\n            cameraHandler = null\n        } catch (e: InterruptedException) {\n            mainThreadHandler.post { cameraErrorListener.onCameraOpenError(e) }\n        }\n    }\n\n    private fun setCameraParameters(\n        camera: Camera,\n        parameters: Camera.Parameters\n    ) {\n        try {\n            camera.parameters = parameters\n        } catch (t: Throwable) {\n            Log.w(Config.logTag, \"Error setting camera parameters\", t)\n            // ignore failure to set camera parameters\n        }\n    }\n\n    private fun startCameraPreview() {\n        cameraHandler?.post {\n            try {\n                retrySync(times = 5) {\n                    mCamera?.startPreview()\n                }\n            } catch (t: Throwable) {\n                mainThreadHandler.post {\n                    cameraErrorListener.onCameraOpenError(t)\n                }\n            }\n        }\n    }\n\n    private fun onCameraOpen(camera: Camera?) {\n        if (camera == null) {\n            mainThreadHandler.post {\n                cameraPreview?.apply { holder.removeCallback(this) }\n                cameraErrorListener.onCameraOpenError(null)\n            }\n        } else {\n            mCamera = camera\n            setCameraDisplayOrientation(activity)\n            setCameraPreviewFrame()\n\n            // Create our Preview view and set it as the content of our activity.\n            cameraPreview = CameraPreview(activity, this).apply {\n                layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)\n            }.also { cameraPreview ->\n                mainThreadHandler.post {\n                    onCameraAvailableListener.get()?.let {\n                        it(camera)\n                    }\n                    onCameraAvailableListener.clear()\n\n                    previewView.removeAllViews()\n                    previewView.addView(cameraPreview)\n                }\n            }\n        }\n    }\n\n    private fun setCameraPreviewFrame() {\n        mCamera?.apply {\n            val format = ImageFormat.NV21\n            val parameters = parameters\n            parameters.previewFormat = format\n\n            val displayMetrics = DisplayMetrics()\n            activity.windowManager.defaultDisplay.getRealMetrics(displayMetrics)\n\n            val displayWidth = max(displayMetrics.heightPixels, displayMetrics.widthPixels)\n            val displayHeight = min(displayMetrics.heightPixels, displayMetrics.widthPixels)\n\n            val height: Int = minimumResolution.height\n            val width = displayWidth * height / displayHeight\n\n            getOptimalPreviewSize(parameters.supportedPreviewSizes, width, height)?.apply {\n                parameters.setPreviewSize(this.width, this.height)\n            }\n\n            setCameraParameters(this, parameters)\n        }\n    }\n\n    private fun getOptimalPreviewSize(\n        sizes: List<Camera.Size>?,\n        w: Int,\n        h: Int\n    ): Camera.Size? {\n        val targetRatio = w.toDouble() / h\n        if (sizes == null) {\n            return null\n        }\n        var optimalSize: Camera.Size? = null\n\n        // Find the smallest size that fits our tolerance and is at least as big as our target\n        // height\n        for (size in sizes) {\n            val ratio = size.width.toDouble() / size.height\n            if (abs(ratio - targetRatio) <= ASPECT_TOLERANCE) {\n                if (size.height >= h) {\n                    optimalSize = size\n                }\n            }\n        }\n\n        // Find the closest ratio that is still taller than our target height\n        if (optimalSize == null) {\n            var minDiffRatio = Double.MAX_VALUE\n            for (size in sizes) {\n                val ratio = size.width.toDouble() / size.height\n                val ratioDiff = abs(ratio - targetRatio)\n                if (size.height >= h && ratioDiff <= minDiffRatio &&\n                    size.height <= MAXIMUM_RESOLUTION.height && size.width <= MAXIMUM_RESOLUTION.width\n                ) {\n                    optimalSize = size\n                    minDiffRatio = ratioDiff\n                }\n            }\n        }\n        if (optimalSize == null) {\n            // Find the smallest size that is at least as big as our target height\n            for (size in sizes) {\n                if (size.height >= h) {\n                    optimalSize = size\n                }\n            }\n        }\n\n        return optimalSize\n    }\n\n    private fun setCameraDisplayOrientation(activity: Activity) {\n        val camera = mCamera ?: return\n        val info = Camera.CameraInfo()\n        Camera.getCameraInfo(Camera.CameraInfo.CAMERA_FACING_BACK, info)\n\n        val rotation = activity.windowManager.defaultDisplay.rotation\n        val degrees = rotation.rotationToDegrees()\n\n        val result = if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {\n            (360 - (info.orientation + degrees) % 360) % 360 // compensate for the mirror\n        } else { // back-facing\n            (info.orientation - degrees + 360) % 360\n        }\n\n        try {\n            camera.stopPreview()\n        } catch (e: java.lang.Exception) {\n            // preview was already stopped\n        }\n\n        try {\n            camera.setDisplayOrientation(result)\n        } catch (t: Throwable) {\n//            cameraErrorListener.onCameraUnsupportedError(t)\n        }\n\n        startCameraPreview()\n\n        mRotation = result\n    }\n\n    /** A basic Camera preview class  */\n    @SuppressLint(\"ViewConstructor\")\n    private inner class CameraPreview(\n        context: Context,\n        private val mPreviewCallback: PreviewCallback\n    ) : SurfaceView(context), AutoFocusCallback, SurfaceHolder.Callback {\n\n        init {\n            holder.addCallback(this)\n            mCamera?.apply {\n                val params = parameters\n                val focusModes = params.supportedFocusModes\n                if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {\n                    params.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE\n                } else if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {\n                    params.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO\n                }\n\n                params.setRecordingHint(true)\n                setCameraParameters(this, params)\n            }\n        }\n\n        override fun onAutoFocus(success: Boolean, camera: Camera) {}\n\n        /**\n         * The Surface has been created, now tell the camera where to draw the preview.\n         */\n        override fun surfaceCreated(holder: SurfaceHolder) {\n            try {\n                mCamera?.setPreviewDisplay(this.holder)\n                mCamera?.setPreviewCallbackWithBuffer(mPreviewCallback)\n                startCameraPreview()\n            } catch (t: Throwable) {\n                mainThreadHandler.post {\n                    cameraErrorListener.onCameraOpenError(t)\n                }\n            }\n        }\n\n        override fun surfaceDestroyed(holder: SurfaceHolder) {\n            // empty. Take care of releasing the Camera preview in your activity.\n        }\n\n        override fun surfaceChanged(\n            holder: SurfaceHolder,\n            format: Int,\n            w: Int,\n            h: Int\n        ) {\n            // If your preview can change or rotate, take care of those events here.\n            // Make sure to stop the preview before resizing or reformatting it.\n            if (this.holder.surface == null) {\n                // preview surface does not exist\n                return\n            }\n\n            // stop preview before making changes\n            try {\n                mCamera?.stopPreview()\n            } catch (t: Throwable) {\n                // ignore: tried to stop a non-existent preview\n            }\n\n            // set preview size and make any resize, rotate or\n            // reformatting changes here\n\n            // start preview with new settings\n            try {\n                mCamera?.setPreviewDisplay(this.holder)\n                val bufSize = w * h * ImageFormat.getBitsPerPixel(format) / 8\n                for (i in 0..2) {\n                    mCamera?.addCallbackBuffer(ByteArray(bufSize))\n                }\n                mCamera?.setPreviewCallbackWithBuffer(mPreviewCallback)\n                startCameraPreview()\n            } catch (t: Throwable) {\n                mainThreadHandler.post {\n                    cameraErrorListener.onCameraOpenError(t)\n                }\n            }\n        }\n    }\n\n    override fun withSupportsMultipleCameras(task: (Boolean) -> Unit) {\n        task(Camera.getNumberOfCameras() > 1)\n    }\n\n    override fun changeCamera() {\n        currentCameraId++\n        if (currentCameraId >= Camera.getNumberOfCameras()) {\n            currentCameraId = 0\n        }\n        onPause()\n        onResume()\n    }\n\n    override fun getCurrentCamera(): Int = currentCameraId\n}\n"
  },
  {
    "path": "scan-camera/src/main/java/com/getbouncer/scan/camera/CameraAdapter.kt",
    "content": "package com.getbouncer.scan.camera\n\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport android.graphics.PointF\nimport android.graphics.Rect\nimport android.util.Log\nimport android.view.Surface\nimport androidx.annotation.CheckResult\nimport androidx.annotation.IntDef\nimport androidx.annotation.MainThread\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleObserver\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.OnLifecycleEvent\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.TrackedImage\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.channels.ClosedSendChannelException\n// TODO: upgrade this when kotlin libs hit 1.5.0\n// import kotlinx.coroutines.channels.onClosed\n// import kotlinx.coroutines.channels.onFailure\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.runBlocking\n\n/**\n * Valid integer rotation values.\n */\n@IntDef(\n    Surface.ROTATION_0,\n    Surface.ROTATION_90,\n    Surface.ROTATION_180,\n    Surface.ROTATION_270\n)\n@Retention(AnnotationRetention.SOURCE)\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nannotation class RotationValue\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class CameraPreviewImage<ImageBase>(\n    val image: TrackedImage<ImageBase>,\n    val previewImageBounds: Rect,\n)\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nabstract class CameraAdapter<CameraOutput> : LifecycleObserver {\n\n    // TODO: change this to be a channelFlow once it's no longer experimental, add some capacity and use a backpressure drop strategy\n    private val imageChannel = Channel<CameraOutput>(capacity = Channel.RENDEZVOUS)\n    private var lifecyclesBound = 0\n\n    abstract val implementationName: String\n\n    companion object {\n\n        /**\n         * Determine if the device supports the camera features used by this SDK.\n         */\n        @JvmStatic\n        fun isCameraSupported(context: Context): Boolean =\n            (context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)).also {\n                if (!it) Log.e(Config.logTag, \"System feature 'FEATURE_CAMERA_ANY' is unavailable\")\n            }\n\n        /**\n         * Calculate degrees from a [RotationValue].\n         */\n        @CheckResult\n        fun Int.rotationToDegrees(): Int = this * 90\n    }\n\n    protected fun sendImageToStream(image: CameraOutput) = try {\n        // TODO: upgrade this when kotlin libs hit 1.5.0\n//        imageChannel.trySend(image).onClosed {\n//            Log.w(Config.logTag, \"Attempted to send image to closed channel\", it)\n//        }.onFailure {\n//            Log.w(Config.logTag, \"Failure when sending image to channel\", it)\n//        }\n        imageChannel.offer(image)\n    } catch (e: ClosedSendChannelException) {\n        Log.w(Config.logTag, \"Attempted to send image to closed channel\")\n    } catch (t: Throwable) {\n        Log.e(Config.logTag, \"Unable to send image to channel\", t)\n    }\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)\n    fun onDestroyed() {\n        runBlocking { imageChannel.close() }\n    }\n\n    /**\n     * Bind this camera manager to a lifecycle.\n     */\n    open fun bindToLifecycle(lifecycleOwner: LifecycleOwner) {\n        lifecycleOwner.lifecycle.addObserver(this)\n        lifecyclesBound++\n    }\n\n    /**\n     * Unbind this camera from a lifecycle. This will pause the camera.\n     */\n    open fun unbindFromLifecycle(lifecycleOwner: LifecycleOwner) {\n        lifecycleOwner.lifecycle.removeObserver(this)\n\n        lifecyclesBound--\n        if (lifecyclesBound < 0) {\n            Log.e(Config.logTag, \"Bound lifecycle count $lifecyclesBound is below 0\")\n            lifecyclesBound = 0\n        }\n\n        this.onPause()\n    }\n\n    /**\n     * Determine if the adapter is currently bound.\n     */\n    open fun isBoundToLifecycle() = lifecyclesBound > 0\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)\n    open fun onPause() {\n        // support OnPause events.\n    }\n\n    /**\n     * Execute a task with flash support.\n     */\n    abstract fun withFlashSupport(task: (Boolean) -> Unit)\n\n    /**\n     * Turn the camera torch on or off.\n     */\n    abstract fun setTorchState(on: Boolean)\n\n    /**\n     * Determine if the torch is currently on.\n     */\n    abstract fun isTorchOn(): Boolean\n\n    /**\n     * Determine if the device has multiple cameras.\n     */\n    abstract fun withSupportsMultipleCameras(task: (Boolean) -> Unit)\n\n    /**\n     * Change to a new camera.\n     */\n    abstract fun changeCamera()\n\n    /**\n     * Determine which camera is currently in use.\n     */\n    abstract fun getCurrentCamera(): Int\n\n    /**\n     * Set the focus on a particular point on the screen.\n     */\n    abstract fun setFocus(point: PointF)\n\n    /**\n     * Get the stream of images from the camera. This is a hot [Flow] of images with a back pressure strategy DROP.\n     * Images that are not read from the flow are dropped. This flow is backed by a [Channel].\n     */\n    fun getImageStream(): Flow<CameraOutput> = imageChannel.receiveAsFlow()\n}\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ninterface CameraErrorListener {\n\n    @MainThread\n    fun onCameraOpenError(cause: Throwable?)\n\n    @MainThread\n    fun onCameraAccessError(cause: Throwable?)\n\n    @MainThread\n    fun onCameraUnsupportedError(cause: Throwable?)\n}\n"
  },
  {
    "path": "scan-camera/src/main/java/com/getbouncer/scan/camera/CameraSelector.kt",
    "content": "package com.getbouncer.scan.camera\n\nimport android.app.Activity\nimport android.graphics.Bitmap\nimport android.os.Build\nimport android.util.Log\nimport android.util.Size\nimport android.view.ViewGroup\nimport com.getbouncer.scan.framework.Config\n\n/**\n * Get the appropriate camera adapter. If the customer has provided an additional camera adapter, use that in place of\n * camera 1.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nfun getCameraAdapter(\n    activity: Activity,\n    previewView: ViewGroup,\n    minimumResolution: Size,\n    cameraErrorListener: CameraErrorListener,\n): CameraAdapter<CameraPreviewImage<Bitmap>> =\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {\n        try {\n            getAlternateCamera(activity, previewView, minimumResolution, cameraErrorListener)\n        } catch (t: Throwable) {\n            Log.d(Config.logTag, \"No alternative camera implementations supplied, falling back to default\", t)\n            Camera1Adapter(activity, previewView, minimumResolution, cameraErrorListener)\n        }\n    } else {\n        Log.d(Config.logTag, \"YUV_420_888 is not supported, falling back to default implementation\")\n        Camera1Adapter(activity, previewView, minimumResolution, cameraErrorListener)\n    }.apply {\n        Log.d(Config.logTag, \"Using camera implementation ${this.implementationName}\")\n    }\n\n@Suppress(\"UNCHECKED_CAST\")\n@Throws(ClassNotFoundException::class, NoSuchMethodException::class, IllegalAccessException::class)\nprivate fun getAlternateCamera(\n    activity: Activity,\n    previewView: ViewGroup,\n    minimumResolution: Size,\n    cameraErrorListener: CameraErrorListener,\n): CameraAdapter<CameraPreviewImage<Bitmap>> =\n    Class.forName(\"com.getbouncer.scan.camera.extension.CameraAdapterImpl\")\n        .getConstructor(\n            Activity::class.java,\n            ViewGroup::class.java,\n            Size::class.java,\n            CameraErrorListener::class.java,\n        )\n        .newInstance(\n            activity,\n            previewView,\n            minimumResolution,\n            cameraErrorListener,\n        ) as CameraAdapter<CameraPreviewImage<Bitmap>>\n"
  },
  {
    "path": "scan-camera2/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "scan-camera2/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\n\nandroid {\n    compileSdkVersion 30\n    buildToolsVersion '30.0.3'\n\n    defaultConfig {\n        minSdkVersion 21\n        targetSdkVersion 30\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles 'consumer-rules.pro'\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    testOptions {\n        unitTests.includeAndroidResources = true\n    }\n\n    lintOptions {\n        enable \"Interoperability\"\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation project(\":scan-framework\")\n    implementation project(':scan-camera')\n\n    implementation \"androidx.appcompat:appcompat:[1.3.0,1.3.1]\"\n    implementation \"androidx.core:core-ktx:[1.3.1,1.6.0]\"\n}\n\ndependencies {\n    testImplementation \"androidx.test:core:1.4.0\"\n    testImplementation \"androidx.test:runner:1.4.0\"\n    testImplementation \"junit:junit:4.13.2\"\n    testImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n}\n\ndependencies {\n    androidTestImplementation \"androidx.test.ext:junit:1.1.3\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.4.0\"\n    androidTestImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n}\n\napply from: 'deploy.gradle'\n"
  },
  {
    "path": "scan-camera2/deploy.gradle",
    "content": "apply plugin: 'maven-publish'\napply plugin: 'org.jetbrains.dokka'\napply plugin: 'signing'\n\ntask androidSourcesJar(type: Jar) {\n    archiveClassifier.set('sources')\n    if (project.plugins.findPlugin(\"com.android.library\")) {\n        // Android library\n        from android.sourceSets.main.java.srcDirs\n        from android.sourceSets.main.kotlin.srcDirs\n    } else {\n        // Pure kotlin library\n        from sourceSets.main.java.srcDirs\n        from sourceSets.main.kotlin.srcDirs\n    }\n}\n\ntasks.withType(dokkaHtmlPartial.getClass()).configureEach {\n    pluginsMapConfiguration.set(\n            [\"org.jetbrains.dokka.base.DokkaBase\": \"\"\"{ \"separateInheritedMembers\": true}\"\"\"]\n    )\n}\n\ntask javadocJar(type: Jar, dependsOn: dokkaJavadoc) {\n    archiveClassifier.set('javadoc')\n    from dokkaJavadoc.outputDirectory\n}\n\nartifacts {\n    archives androidSourcesJar\n    archives javadocJar\n}\n\next[\"signing.keyId\"] = ''\next[\"signing.password\"] = ''\next[\"signing.secretKeyRingFile\"] = ''\n\next[\"ossrhUsername\"] = ''\next[\"ossrhPassword\"] = ''\next[\"sonatypeStagingProfileId\"] = ''\n\next {\n\n    libraryDescription = 'This library provides the framework for cameras'\n\n    siteUrl = 'https://getbouncer.com'\n\n    scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'\n    scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'\n    scmUrl = 'https://github.com/getbouncer/cardscan-android'\n\n    licenseName = 'bouncer-free-1'\n    licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'\n\n    developerId = 'getbouncer'\n    developerName = 'Bouncer Technologies'\n    developerEmail = 'bouncer-support@stripe.com'\n\n    publishGroupId = 'com.getbouncer'\n    publishArtifactId = 'scan-camera2'\n    publishVersion = version\n}\n\ngroup = publishGroupId\nversion = publishVersion\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    Properties p = new Properties()\n    new FileInputStream(secretPropsFile).withCloseable { is ->\n        p.load(is)\n    }\n    p.each { name, value ->\n        ext[name] = value\n    }\n} else {\n    ext[\"signing.keyId\"] = System.getenv('SIGNING_KEY_ID')\n    ext[\"signing.password\"] = System.getenv('SIGNING_PASSWORD')\n    ext[\"signing.secretKeyRingFile\"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')\n\n    ext[\"ossrhUsername\"] = System.getenv('OSSRH_USERNAME')\n    ext[\"ossrhPassword\"] = System.getenv('OSSRH_PASSWORD')\n\n    ext[\"sonatypeStagingProfileId\"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')\n}\n\npublishing {\n    publications {\n        release(MavenPublication) {\n            groupId publishGroupId\n            artifactId publishArtifactId\n            version publishVersion\n\n            // Two artifacts, the `aar` (or `jar`) and the sources\n            if (project.plugins.findPlugin(\"com.android.library\")) {\n                artifact(\"$buildDir/outputs/aar/${project.getName()}-release.aar\")\n            } else {\n                artifact(\"$buildDir/libs/${project.getName()}-${version}.jar\")\n            }\n            artifact androidSourcesJar\n\n            pom {\n                name = publishArtifactId\n                description = libraryDescription\n                url = siteUrl\n                licenses {\n                    license {\n                        name = licenseName\n                        url = licenseUrl\n                    }\n                }\n                developers {\n                    developer {\n                        id = developerId\n                        name = developerName\n                        email = developerEmail\n                    }\n                }\n                scm {\n                    connection = scmConnection\n                    developerConnection = scmDeveloperConnection\n                    url = scmUrl\n                }\n                // A slightly hacky fix so that your POM will include any transitive dependencies\n                // that your library builds upon\n                withXml {\n                    def dependenciesNode = asNode().appendNode('dependencies')\n\n                    project.configurations.implementation.allDependencies.each {\n                        if (it.group != null && it.version != null) {\n                            def dependencyNode = dependenciesNode.appendNode('dependency')\n                            dependencyNode.appendNode('groupId', it.group)\n                            dependencyNode.appendNode('artifactId', it.name)\n                            dependencyNode.appendNode('version', it.version)\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // The repository to publish to, Sonatype/MavenCentral\n    repositories {\n        maven {\n            // This is an arbitrary name, you may also use \"mavencentral\" or\n            // any other name that's descriptive for you\n            name = \"sonatype\"\n            url = \"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/\"\n            credentials {\n                username ossrhUsername\n                password ossrhPassword\n            }\n        }\n    }\n}\n\nsigning {\n    sign publishing.publications\n}\n"
  },
  {
    "path": "scan-camera2/src/androidTest/java/com/getbouncer/scan/camera/extension/UtilInstrumentationTest.kt",
    "content": "package com.getbouncer.scan.camera.extension\n\nimport android.util.Size\nimport android.view.Surface\nimport androidx.test.filters.SmallTest\nimport org.junit.Test\nimport kotlin.test.assertEquals\n\nclass UtilInstrumentationTest {\n\n    @Test\n    @SmallTest\n    fun resolutionToSize_perpendicular() {\n        assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 90))\n        assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 270))\n        assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 0))\n        assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 180))\n        assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 90))\n        assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 270))\n        assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 0))\n        assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 180))\n    }\n\n    @Test\n    @SmallTest\n    fun resolutionToSize_parallel() {\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 0))\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 180))\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 90))\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 270))\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 0))\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 180))\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 90))\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 270))\n    }\n\n    @Test\n    @SmallTest\n    fun resolutionToSize_oblique() {\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 45))\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 135))\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 45))\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 135))\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 45))\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 135))\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 45))\n        assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 135))\n    }\n}\n"
  },
  {
    "path": "scan-camera2/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.getbouncer.scan.camera.extension\" >\n\n    <uses-permission android:name=\"android.permission.CAMERA\" />\n    <uses-permission android:name=\"android.permission.FLASHLIGHT\" />\n\n    <uses-feature android:name=\"android.hardware.camera\" android:required=\"false\" />\n    <uses-feature android:name=\"android.hardware.camera.autofocus\" android:required=\"false\" />\n\n</manifest>\n"
  },
  {
    "path": "scan-camera2/src/main/java/com/getbouncer/scan/camera/extension/CameraAdapterImpl.kt",
    "content": "/*\n * Copyright 2014 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 com.getbouncer.scan.camera.extension\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.ImageFormat\nimport android.graphics.Matrix\nimport android.graphics.PointF\nimport android.graphics.Rect\nimport android.graphics.SurfaceTexture\nimport android.hardware.camera2.CameraAccessException\nimport android.hardware.camera2.CameraCaptureSession\nimport android.hardware.camera2.CameraCharacteristics\nimport android.hardware.camera2.CameraDevice\nimport android.hardware.camera2.CameraManager\nimport android.hardware.camera2.CameraMetadata\nimport android.hardware.camera2.CaptureRequest\nimport android.hardware.camera2.TotalCaptureResult\nimport android.hardware.camera2.params.MeteringRectangle\nimport android.hardware.camera2.params.StreamConfigurationMap\nimport android.media.ImageReader\nimport android.os.Build\nimport android.os.Handler\nimport android.os.HandlerThread\nimport android.util.Log\nimport android.util.Size\nimport android.view.Surface\nimport android.view.TextureView\nimport android.view.ViewGroup\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleObserver\nimport androidx.lifecycle.OnLifecycleEvent\nimport com.getbouncer.scan.camera.CameraAdapter\nimport com.getbouncer.scan.camera.CameraErrorListener\nimport com.getbouncer.scan.camera.CameraPreviewImage\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.Stats\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.image.getRenderScript\nimport com.getbouncer.scan.framework.image.isSupportedFormat\nimport com.getbouncer.scan.framework.image.rotate\nimport com.getbouncer.scan.framework.image.toBitmap\nimport com.getbouncer.scan.framework.util.scale\nimport com.getbouncer.scan.framework.util.scaleAndCenterSurrounding\nimport com.getbouncer.scan.framework.util.size\nimport com.getbouncer.scan.framework.util.toRectF\nimport java.util.Locale\nimport java.util.concurrent.Semaphore\nimport java.util.concurrent.TimeUnit\nimport java.util.concurrent.atomic.AtomicBoolean\nimport kotlin.math.max\n\n/**\n * For tap to focus.\n */\nprivate const val FOCUS_TOUCH_SIZE = 150\n\n/**\n * The default image format. This is not necessarily the fastest to process, but most supported.\n */\nconst val DEFAULT_IMAGE_FORMAT = ImageFormat.YUV_420_888\n\n/**\n * Unable to open the camera.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass CameraDeviceCallbackOpenException(val cameraId: String, val errorCode: Int) : Exception() {\n    override fun toString(): String {\n        return \"CameraDeviceCallbackOpenException(cameraId='$cameraId', errorCode=$errorCode)\"\n    }\n}\n\n/**\n * Unable to configure the camera.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass CameraConfigurationFailedException(val cameraId: String) : Exception() {\n    override fun toString(): String {\n        return \"CameraConfigurationFailedException(cameraId='$cameraId')\"\n    }\n}\n\n/**\n * A [CameraAdapter] that uses android's Camera 2 APIs to show previews and process images.\n */\ninternal class CameraAdapterImpl(\n    private val activity: Activity,\n    private val previewView: ViewGroup,\n    private val minimumResolution: Size,\n    private val cameraErrorListener: CameraErrorListener,\n) : CameraAdapter<CameraPreviewImage<Bitmap>>(), LifecycleObserver {\n\n    override val implementationName: String = \"Camera2\"\n\n    private val previewTextureView by lazy { TextureView(activity) }\n\n    private val processingImage = AtomicBoolean(false)\n\n    private val displayRotation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n        activity.display?.rotation\n    } else {\n        null\n    } ?: @Suppress(\"Deprecation\") activity.windowManager.defaultDisplay.rotation\n\n    private lateinit var cameraId: String\n\n    private var previewCaptureSession: CameraCaptureSession? = null\n\n    private var cameraDevice: CameraDevice? = null\n\n    private lateinit var previewSize: Size\n\n    private lateinit var previewResolution: Size\n\n    private var cameraThread: HandlerThread? = null\n\n    private var cameraHandler: Handler? = null\n\n    private var imageReader: ImageReader? = null\n\n    private var sensorRotation = 0\n\n    private val cameraOpenCloseLock = Semaphore(1)\n\n    private var flashSupported = false\n\n    private lateinit var previewRequestBuilder: CaptureRequest.Builder\n\n    private var onInitializedFlashTask: ((Boolean) -> Unit)? = null\n\n    private var autoFocusMode: Int = CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE\n\n    private val mainThreadHandler = Handler(activity.mainLooper)\n\n    private var focusPoint = PointF(previewView.width / 2F, previewView.height / 2F)\n\n    private var currentCameraIndex = -1\n\n    private lateinit var scaledPreviewSize: Rect\n\n    private val previewSurfaceTextureListener = object : TextureView.SurfaceTextureListener {\n        override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {\n            openCamera()\n        }\n\n        override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {\n            configureTransform(Size(width, height), previewSize)\n        }\n\n        override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean = true\n\n        override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { }\n    }\n\n    private val imageAvailableListener = ImageReader.OnImageAvailableListener { reader ->\n        if (!processingImage.compareAndSet(false, true)) {\n            return@OnImageAvailableListener\n        }\n\n        reader.acquireLatestImage()?.let {\n            try {\n                it.toBitmap(getRenderScript(activity)).rotate(calculateImageRotationDegrees(displayRotation, sensorRotation).toFloat())\n            } catch (t: Throwable) {\n                Log.e(Config.logTag, \"Unable to convert image to bitmap: $t\")\n                null\n            } finally {\n                it.close()\n            }\n        }?.let {\n            sendImageToStream(\n                CameraPreviewImage(\n                    TrackedImage(it, Stats.trackRepeatingTask(\"image_analysis\")),\n                    scaledPreviewSize,\n                ),\n            )\n        }\n\n        processingImage.set(false)\n    }\n\n    private val stateCallback = object : CameraDevice.StateCallback() {\n\n        @Synchronized\n        override fun onOpened(camera: CameraDevice) {\n            cameraOpenCloseLock.release()\n            cameraDevice = camera\n            createCameraPreviewSession(previewResolution)\n\n            onInitializedFlashTask?.apply {\n                mainThreadHandler.post { this(flashSupported) }\n            }\n        }\n\n        override fun onDisconnected(camera: CameraDevice) {\n            cameraOpenCloseLock.release()\n            camera.close()\n            cameraDevice = null\n        }\n\n        override fun onError(camera: CameraDevice, error: Int) {\n            onDisconnected(camera)\n            mainThreadHandler.post {\n                cameraErrorListener.onCameraOpenError(CameraDeviceCallbackOpenException(camera.id, error))\n            }\n        }\n    }\n\n    @Synchronized\n    override fun withFlashSupport(task: (Boolean) -> Unit) {\n        if (::previewRequestBuilder.isInitialized) {\n            mainThreadHandler.post { task(flashSupported) }\n        } else {\n            onInitializedFlashTask = task\n        }\n    }\n\n    override fun setTorchState(on: Boolean) {\n        if (!::previewRequestBuilder.isInitialized) {\n            return\n        }\n\n        if (on) {\n            previewRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)\n        } else {\n            previewRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF)\n        }\n        previewCaptureSession?.setRepeatingRequest(previewRequestBuilder.build(), null, cameraHandler)\n    }\n\n    override fun isTorchOn() =\n        if (::previewRequestBuilder.isInitialized) {\n            previewRequestBuilder.get(CaptureRequest.FLASH_MODE) == CaptureRequest.FLASH_MODE_TORCH\n        } else {\n            false\n        }\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)\n    fun onCreate() {\n        previewView.removeAllViews()\n        previewView.addView(previewTextureView)\n    }\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)\n    fun onResume() {\n        startCameraThread()\n\n        if (previewTextureView.isAvailable) {\n            openCamera()\n        } else {\n            previewTextureView.surfaceTextureListener = previewSurfaceTextureListener\n        }\n    }\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)\n    override fun onPause() {\n        super.onPause()\n        closeCamera()\n        stopCameraThread()\n    }\n\n    /**\n     * Sets up member variables related to camera.\n     */\n    private fun setUpCameraOutputs() {\n        try {\n            getCurrentCameraDetails()?.also { cameraDetails ->\n                sensorRotation = cameraDetails.sensorRotation\n                autoFocusMode = selectAutoFocusMode(cameraDetails.supportedAutoFocusModes)\n\n                // Given a desired resolution, get a resolution and format that the camera supports.\n                val previewFormatAndResolution = getOptimalPreviewResolution(\n                    getCameraResolutions(cameraDetails.config),\n                    minimumResolution\n                )\n\n                // rotate the preview resolution to match the orientation\n                val previewFormat = previewFormatAndResolution.first\n                previewResolution = previewFormatAndResolution.second\n                previewSize = previewResolution.resolutionToSize(\n                    displayRotation,\n                    cameraDetails.sensorRotation,\n                )\n                Log.d(Config.logTag, \"Camera2 API selected resolution $previewResolution with format $previewFormat\")\n\n                imageReader = ImageReader.newInstance(previewResolution.width, previewResolution.height, previewFormat, 1)\n                    .apply {\n                        setOnImageAvailableListener(imageAvailableListener, cameraHandler)\n                    }\n\n                previewTextureView.layoutParams.apply {\n                    width = ViewGroup.LayoutParams.MATCH_PARENT\n                    height = ViewGroup.LayoutParams.MATCH_PARENT\n                }\n\n                previewTextureView.requestLayout()\n\n                // Check if the flash is supported.\n                flashSupported = cameraDetails.flashAvailable\n\n                cameraId = cameraDetails.cameraId\n            }\n        } catch (e: CameraAccessException) {\n            mainThreadHandler.post { cameraErrorListener.onCameraAccessError(e) }\n        } catch (e: NullPointerException) {\n            // Currently an NPE is thrown when the Camera2API is used but not supported on the\n            // device this code runs.\n            mainThreadHandler.post { cameraErrorListener.onCameraUnsupportedError(e) }\n        }\n    }\n\n    private fun selectAutoFocusMode(supportedAutoFocusModes: List<Int>): Int =\n        when {\n            CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE in supportedAutoFocusModes ->\n                CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE\n            CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_VIDEO in supportedAutoFocusModes ->\n                CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_VIDEO\n            else -> CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE\n        }\n\n    private fun getCameraManager() = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager\n\n    /**\n     * Get a list of cameraIds and their configuration maps.\n     */\n    private val availableCameras: List<CameraDetails> by lazy {\n        val manager = getCameraManager()\n        manager.cameraIdList\n            .map { it to manager.getCameraCharacteristics(it) }\n            .mapNotNull {\n                it.second.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)?.run {\n                    CameraDetails(\n                        cameraId = it.first,\n                        flashAvailable = it.second.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true,\n                        config = this,\n                        sensorRotation = it.second.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0,\n                        supportedAutoFocusModes = it.second.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)?.toList() ?: emptyList(),\n                        lensFacing = it.second.get(CameraCharacteristics.LENS_FACING),\n                    )\n                }\n            }\n    }\n\n    private val defaultCameraIndex by lazy {\n        availableCameras.indexOfFirst {\n            it.lensFacing != CameraCharacteristics.LENS_FACING_FRONT\n        }\n    }\n\n    private fun getCurrentCameraDetails(): CameraDetails? = availableCameras.let {\n        if (currentCameraIndex < 0) {\n            currentCameraIndex = if (defaultCameraIndex >= 0) defaultCameraIndex else 0\n        }\n\n        if (currentCameraIndex < it.size) it[currentCameraIndex] else null\n    }\n\n    /**\n     * Get a list of all the supported camera resolutions for each format. Output is in the format:\n     *\n     * ```\n     * [\n     *   (CameraFormat, Resolution),\n     *   (CameraFormat, Resolution),\n     *   ...\n     * ]\n     * ```\n     *\n     * Note that each format will likely have multiple resolutions. Available formats will be sorted\n     * by preference (fastest first).\n     */\n    private fun getCameraResolutions(map: StreamConfigurationMap): List<Pair<Int, Size>> {\n        val formats = map.outputFormats.filter { isSupportedFormat(it) }.sortedBy {\n            when (it) {\n                ImageFormat.NV21 -> 0\n                ImageFormat.YUV_420_888 -> 1\n                ImageFormat.JPEG -> 2\n                ImageFormat.YUY2 -> 3\n                else -> it\n            }\n        }\n        val formatToOutputSizes = formats.map { format ->\n            (map.getOutputSizes(format) ?: emptyArray())\n                .asIterable()\n                .map { format to it }\n        }\n        return formatToOutputSizes.flatten()\n    }\n\n    /**\n     * Opens the camera specified by [cameraId].\n     */\n    @SuppressLint(\"MissingPermission\")\n    private fun openCamera() {\n        setUpCameraOutputs()\n\n        previewView.apply { configureTransform(size(), previewSize) }\n\n        val manager = getCameraManager()\n        try {\n            // Wait for camera to open - 2.5 seconds is sufficient\n            if (!cameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {\n                mainThreadHandler.post { cameraErrorListener.onCameraOpenError(null) }\n                return\n            }\n            manager.openCamera(cameraId, stateCallback, cameraHandler)\n        } catch (e: CameraAccessException) {\n            mainThreadHandler.post { cameraErrorListener.onCameraAccessError(e) }\n        } catch (e: InterruptedException) {\n            mainThreadHandler.post { cameraErrorListener.onCameraOpenError(e) }\n        }\n    }\n\n    /**\n     * Closes the current [CameraDevice].\n     */\n    private fun closeCamera() {\n        try {\n            cameraOpenCloseLock.acquire()\n            previewCaptureSession?.close()\n            previewCaptureSession = null\n            cameraDevice?.close()\n            cameraDevice = null\n        } catch (e: InterruptedException) {\n            mainThreadHandler.post {\n                cameraErrorListener.onCameraOpenError(e)\n            }\n        } finally {\n            cameraOpenCloseLock.release()\n        }\n    }\n\n    /**\n     * Starts a background thread and its [Handler].\n     */\n    private fun startCameraThread() {\n        val thread = HandlerThread(\"CameraBackground\").also { it.start() }\n        cameraThread = thread\n        cameraHandler = Handler(thread.looper)\n    }\n\n    /**\n     * Stops the background thread and its [Handler].\n     */\n    private fun stopCameraThread() {\n        cameraThread?.quitSafely()\n        try {\n            cameraThread?.join()\n            cameraThread = null\n            cameraHandler = null\n        } catch (e: InterruptedException) {\n            mainThreadHandler.post { cameraErrorListener.onCameraOpenError(e) }\n        }\n    }\n\n    /**\n     * Creates a new [CameraCaptureSession] for camera preview.\n     */\n    private fun createCameraPreviewSession(previewResolution: Size) {\n        try {\n            val previewTexture = previewTextureView.surfaceTexture\n\n            // We configure the size of default buffer to be the size of camera preview we want.\n            previewTexture?.setDefaultBufferSize(previewResolution.width, previewResolution.height)\n\n            // This is the output Surface we need to start preview.\n            val previewSurface = previewTexture?.let { Surface(it) }\n            val imageReaderSurface = imageReader?.surface\n\n            // We set up a CaptureRequest.Builder with the output Surface.\n            previewRequestBuilder = cameraDevice!!.createCaptureRequest(\n                CameraDevice.TEMPLATE_PREVIEW\n            )\n\n            previewSurface?.apply { previewRequestBuilder.addTarget(this) }\n            imageReaderSurface?.apply { previewRequestBuilder.addTarget(this) }\n\n            // Here, we create a CameraCaptureSession for camera preview.\n            @Suppress(\"Deprecation\") // SessionConfiguration is not available until API 28.\n            cameraDevice?.createCaptureSession(\n                listOfNotNull(imageReaderSurface, previewSurface),\n                object : CameraCaptureSession.StateCallback() {\n\n                    override fun onConfigured(cameraCaptureSession: CameraCaptureSession) {\n                        // The camera is already closed\n                        if (cameraDevice == null) return\n\n                        // When the session is ready, we start displaying the preview.\n                        previewCaptureSession = cameraCaptureSession\n                        try {\n                            // Auto focus should be continuous for camera preview. This does not\n                            // work on samsung devices.\n                            if (!Build.MANUFACTURER.toUpperCase(Locale.ROOT).contains(\"SAMSUNG\")) {\n                                previewRequestBuilder.set(\n                                    CaptureRequest.CONTROL_AF_MODE,\n                                    autoFocusMode\n                                )\n                            }\n\n                            // Finally, we start displaying the camera preview.\n                            val previewRequest = previewRequestBuilder.build()\n                            previewCaptureSession?.setRepeatingRequest(previewRequest, null, cameraHandler)\n                        } catch (e: CameraAccessException) {\n                            // Ignore camera access errors, this occurs when the camera is closed and will fire again\n                            // when the camera is opened.\n                        }\n                    }\n\n                    override fun onConfigureFailed(session: CameraCaptureSession) {\n                        mainThreadHandler.post {\n                            cameraErrorListener.onCameraOpenError(\n                                CameraConfigurationFailedException(\n                                    session.device.id\n                                )\n                            )\n                        }\n                    }\n                },\n                null\n            )\n        } catch (e: CameraAccessException) {\n            mainThreadHandler.post { cameraErrorListener.onCameraAccessError(e) }\n        }\n    }\n\n    /**\n     * Configures the necessary [android.graphics.Matrix] transformation to `textureView`.\n     * This method should be called after the camera preview size is determined in\n     * setUpCameraOutputs and also the size of `textureView` is fixed.\n     *\n     * @param viewSize The size of `textureView`\n     */\n    private fun configureTransform(viewSize: Size, imageSize: Size) {\n        val matrix = Matrix()\n        val viewRect = viewSize.toRectF()\n        val bufferRect = imageSize.toRectF()\n\n        val rotation = -(displayRotation.rotationToDegrees()).toFloat()\n        val imageScale = calculatePreviewScale(\n            viewSize = viewSize,\n            imageSize = imageSize,\n            displayRotation = displayRotation,\n            sensorRotationDegrees = sensorRotation,\n        )\n        val finalScale = imageScale.scale(\n            max(\n                imageScale.width * imageSize.width / viewSize.width,\n                imageScale.height * imageSize.height / viewSize.height,\n            )\n        )\n\n        // TODO(awushensky): this breaks on rotation. See https://stackoverflow.com/questions/34536798/android-camera2-preview-is-rotated-90deg-while-in-landscape?rq=1\n        bufferRect.offset(viewRect.centerX() - bufferRect.centerX(), viewRect.centerY() - bufferRect.centerY())\n        matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.CENTER)\n        matrix.postScale(finalScale.width, finalScale.height, viewRect.centerX(), viewRect.centerY())\n        matrix.postRotate(rotation, viewRect.centerX(), viewRect.centerY())\n\n        scaledPreviewSize = imageSize.scaleAndCenterSurrounding(viewSize)\n        previewTextureView.setTransform(matrix)\n    }\n\n    override fun setFocus(point: PointF) {\n        focusPoint = point\n        updateFocus(point)\n    }\n\n    private fun updateFocus(point: PointF) {\n        if (!::previewRequestBuilder.isInitialized) {\n            return\n        }\n\n        previewCaptureSession?.stopRepeating()\n\n        previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL)\n        previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF)\n        previewCaptureSession?.capture(previewRequestBuilder.build(), null, cameraHandler)\n\n        if (isMeteringAreaAFSupported()) {\n            previewRequestBuilder.set(\n                CaptureRequest.CONTROL_AF_REGIONS,\n                arrayOf(\n                    MeteringRectangle(\n                        max(point.x.toInt() - FOCUS_TOUCH_SIZE, 0),\n                        max(point.y.toInt() - FOCUS_TOUCH_SIZE, 0),\n                        FOCUS_TOUCH_SIZE * 2,\n                        FOCUS_TOUCH_SIZE * 2,\n                        MeteringRectangle.METERING_WEIGHT_MAX - 1\n                    )\n                )\n            )\n        }\n\n        previewRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)\n        previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO)\n        previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START)\n        previewRequestBuilder.setTag(\"FOCUS_TAG\") // we'll capture this later for resuming the preview\n\n        previewCaptureSession?.capture(\n            previewRequestBuilder.build(),\n            object : CameraCaptureSession.CaptureCallback() {\n                override fun onCaptureCompleted(\n                    session: CameraCaptureSession,\n                    request: CaptureRequest,\n                    result: TotalCaptureResult,\n                ) {\n                    super.onCaptureCompleted(session, request, result)\n\n                    if (request.tag == \"FOCUS_TAG\") {\n                        // the focus trigger is complete -\n                        // resume repeating (preview surface will get frames), clear AF trigger\n                        previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, null)\n                        previewCaptureSession?.setRepeatingRequest(previewRequestBuilder.build(), null, cameraHandler)\n                    }\n                }\n            },\n            cameraHandler\n        )\n    }\n\n    private fun isMeteringAreaAFSupported(): Boolean {\n        val manager = getCameraManager()\n        val cameraCharacteristics = manager.getCameraCharacteristics(cameraId)\n        return cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) ?: 0 >= 1\n    }\n\n    override fun withSupportsMultipleCameras(task: (Boolean) -> Unit) {\n        task(availableCameras.size > 1)\n    }\n\n    override fun changeCamera() {\n        onPause()\n\n        currentCameraIndex++\n        if (currentCameraIndex >= availableCameras.size) {\n            currentCameraIndex = 0\n        }\n\n        onResume()\n    }\n\n    override fun getCurrentCamera(): Int = currentCameraIndex\n}\n"
  },
  {
    "path": "scan-camera2/src/main/java/com/getbouncer/scan/camera/extension/CameraDetails.kt",
    "content": "package com.getbouncer.scan.camera.extension\n\nimport android.hardware.camera2.params.StreamConfigurationMap\n\n/**\n * Details about a camera.\n */\ninternal data class CameraDetails(\n    val cameraId: String,\n    val flashAvailable: Boolean,\n    val config: StreamConfigurationMap,\n    val sensorRotation: Int,\n    val supportedAutoFocusModes: List<Int>,\n    val lensFacing: Int?,\n)\n"
  },
  {
    "path": "scan-camera2/src/main/java/com/getbouncer/scan/camera/extension/Util.kt",
    "content": "package com.getbouncer.scan.camera.extension\n\nimport android.util.Size\nimport android.util.SizeF\nimport android.view.Surface\nimport androidx.annotation.CheckResult\nimport com.getbouncer.scan.camera.RotationValue\n\n/**\n * The maximum resolution width for a preview.\n */\nprivate const val MAX_RESOLUTION_WIDTH = 1920\n\n/**\n * The maximum resolution height for a preview.\n */\nprivate const val MAX_RESOLUTION_HEIGHT = 1080\n\n/**\n * Calculate how much an image must scale in X and Y to match a view size.\n */\n@CheckResult\ninternal fun calculatePreviewScale(\n    viewSize: Size,\n    imageSize: Size,\n    @RotationValue displayRotation: Int,\n    sensorRotationDegrees: Int\n) = if (areScreenAndSensorPerpendicular(displayRotation, sensorRotationDegrees)) {\n    SizeF(viewSize.height.toFloat() / imageSize.height, viewSize.width.toFloat() / imageSize.width)\n} else {\n    SizeF(viewSize.width.toFloat() / imageSize.width, viewSize.height.toFloat() / imageSize.height)\n}\n\n/**\n * Convert a resolution to a size on the screen.\n */\n@CheckResult\ninternal fun Size.resolutionToSize(\n    @RotationValue displayRotation: Int,\n    sensorRotationDegrees: Int\n) = if (areScreenAndSensorPerpendicular(displayRotation, sensorRotationDegrees)) {\n    Size(this.height, this.width)\n} else {\n    this\n}\n\n/**\n * Determines if the dimensions are swapped given the phone's current rotation.\n *\n * @param displayRotation The current rotation of the display\n *\n * @return true if the dimensions are swapped, false otherwise.\n */\n@CheckResult\ninternal fun areScreenAndSensorPerpendicular(\n    @RotationValue displayRotation: Int,\n    sensorRotationDegrees: Int\n) = when (displayRotation) {\n    Surface.ROTATION_0, Surface.ROTATION_180 -> {\n        sensorRotationDegrees == 90 || sensorRotationDegrees == 270\n    }\n    Surface.ROTATION_90, Surface.ROTATION_270 -> {\n        sensorRotationDegrees == 0 || sensorRotationDegrees == 180\n    }\n    else -> {\n        false\n    }\n}\n\n/**\n * Determine how much to rotate the image from the camera given the orientation of the\n * display and the orientation of the camera sensor.\n *\n * @param displayOrientation: The enum value of the display rotation (e.g. Surface.ROTATION_0)\n * @param sensorRotationDegrees: The rotation of the sensor in degrees\n *\n * @return the difference in degrees.\n */\n@CheckResult\ninternal fun calculateImageRotationDegrees(\n    @RotationValue displayOrientation: Int,\n    sensorRotationDegrees: Int\n) = (\n    (\n        when (displayOrientation) {\n            Surface.ROTATION_0 -> sensorRotationDegrees\n            Surface.ROTATION_90 -> sensorRotationDegrees - 90\n            Surface.ROTATION_180 -> sensorRotationDegrees - 180\n            Surface.ROTATION_270 -> sensorRotationDegrees - 270\n            else -> 0\n        } % 360\n        ) + 360\n    ) % 360\n\n/**\n * Get the optimal preview resolution from a list of available formats and resolutions.\n */\n@CheckResult\ninternal fun getOptimalPreviewResolution(\n    cameraSizes: Iterable<Pair<Int, Size>>,\n    minimumResolution: Size\n): Pair<Int, Size> {\n    // Only consider camera resolutions larger than the minimum resolution, but smaller than\n    // the maximum resolution.\n    val allowedCameraSizes = cameraSizes.filter {\n        it.second.width <= MAX_RESOLUTION_WIDTH &&\n            it.second.height <= MAX_RESOLUTION_HEIGHT &&\n            it.second.width >= minimumResolution.width &&\n            it.second.height >= minimumResolution.height\n    }\n\n    return allowedCameraSizes.minByOrNull {\n        it.second.width * it.second.height\n    } ?: DEFAULT_IMAGE_FORMAT to Size(MAX_RESOLUTION_WIDTH, MAX_RESOLUTION_HEIGHT)\n}\n"
  },
  {
    "path": "scan-camera2/src/test/java/com/getbouncer/scan/camera/extension/UtilTest.kt",
    "content": "package com.getbouncer.scan.camera.extension\n\nimport android.view.Surface\nimport androidx.test.filters.SmallTest\nimport org.junit.Test\nimport kotlin.test.assertEquals\n\nclass UtilTest {\n\n    @Test\n    @SmallTest\n    fun calculateImageRotationDegrees_vertical() {\n        assertEquals(0, calculateImageRotationDegrees(Surface.ROTATION_0, 0))\n        assertEquals(45, calculateImageRotationDegrees(Surface.ROTATION_0, 45))\n        assertEquals(315, calculateImageRotationDegrees(Surface.ROTATION_0, -45))\n        assertEquals(180, calculateImageRotationDegrees(Surface.ROTATION_0, 180))\n        assertEquals(180, calculateImageRotationDegrees(Surface.ROTATION_0, -180))\n    }\n\n    @Test\n    @SmallTest\n    fun calculateImageRotationDegrees_right() {\n        assertEquals(270, calculateImageRotationDegrees(Surface.ROTATION_90, 0))\n        assertEquals(315, calculateImageRotationDegrees(Surface.ROTATION_90, 45))\n        assertEquals(225, calculateImageRotationDegrees(Surface.ROTATION_90, -45))\n        assertEquals(90, calculateImageRotationDegrees(Surface.ROTATION_90, 180))\n        assertEquals(90, calculateImageRotationDegrees(Surface.ROTATION_90, -180))\n    }\n\n    @Test\n    @SmallTest\n    fun calculateImageRotationDegrees_left() {\n        assertEquals(90, calculateImageRotationDegrees(Surface.ROTATION_270, 0))\n        assertEquals(135, calculateImageRotationDegrees(Surface.ROTATION_270, 45))\n        assertEquals(45, calculateImageRotationDegrees(Surface.ROTATION_270, -45))\n        assertEquals(270, calculateImageRotationDegrees(Surface.ROTATION_270, 180))\n        assertEquals(270, calculateImageRotationDegrees(Surface.ROTATION_270, -180))\n    }\n\n    @Test\n    @SmallTest\n    fun calculateImageRotationDegrees_inverted() {\n        assertEquals(180, calculateImageRotationDegrees(Surface.ROTATION_180, 0))\n        assertEquals(225, calculateImageRotationDegrees(Surface.ROTATION_180, 45))\n        assertEquals(135, calculateImageRotationDegrees(Surface.ROTATION_180, -45))\n        assertEquals(0, calculateImageRotationDegrees(Surface.ROTATION_180, 180))\n        assertEquals(0, calculateImageRotationDegrees(Surface.ROTATION_180, -180))\n    }\n}\n"
  },
  {
    "path": "scan-camerax/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "scan-camerax/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\n\nandroid {\n    compileSdkVersion 30\n    buildToolsVersion '30.0.3'\n\n    defaultConfig {\n        minSdkVersion 21\n        targetSdkVersion 30\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles 'consumer-rules.pro'\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    testOptions {\n        unitTests.includeAndroidResources = true\n    }\n\n    lintOptions {\n        enable \"Interoperability\"\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = JavaVersion.VERSION_1_8.toString()\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation project(\":scan-framework\")\n    implementation project(':scan-camera')\n\n    implementation \"androidx.appcompat:appcompat:[1.3.0,1.3.1]\"\n    implementation \"androidx.camera:camera-camera2:[1.0.0,1.0.1]\"\n    implementation \"androidx.camera:camera-core:[1.0.0,1.0.1]\"\n    implementation \"androidx.camera:camera-lifecycle:[1.0.0,1.0.1]\"\n    implementation \"androidx.camera:camera-view:[1.0.0-alpha26,1.0.0-alpha28)\"\n    implementation \"androidx.core:core-ktx:[1.3.1,1.6.0]\"\n}\n\ndependencies {\n    testImplementation \"androidx.test:core:1.4.0\"\n    testImplementation \"androidx.test:runner:1.4.0\"\n    testImplementation \"junit:junit:4.13.2\"\n    testImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n}\n\ndependencies {\n    androidTestImplementation \"androidx.test.ext:junit:1.1.3\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.4.0\"\n    androidTestImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n}\n\napply from: 'deploy.gradle'\n"
  },
  {
    "path": "scan-camerax/deploy.gradle",
    "content": "apply plugin: 'maven-publish'\napply plugin: 'org.jetbrains.dokka'\napply plugin: 'signing'\n\ntask androidSourcesJar(type: Jar) {\n    archiveClassifier.set('sources')\n    if (project.plugins.findPlugin(\"com.android.library\")) {\n        // Android library\n        from android.sourceSets.main.java.srcDirs\n        from android.sourceSets.main.kotlin.srcDirs\n    } else {\n        // Pure kotlin library\n        from sourceSets.main.java.srcDirs\n        from sourceSets.main.kotlin.srcDirs\n    }\n}\n\ntasks.withType(dokkaHtmlPartial.getClass()).configureEach {\n    pluginsMapConfiguration.set(\n            [\"org.jetbrains.dokka.base.DokkaBase\": \"\"\"{ \"separateInheritedMembers\": true}\"\"\"]\n    )\n}\n\ntask javadocJar(type: Jar, dependsOn: dokkaJavadoc) {\n    archiveClassifier.set('javadoc')\n    from dokkaJavadoc.outputDirectory\n}\n\nartifacts {\n    archives androidSourcesJar\n    archives javadocJar\n}\n\next[\"signing.keyId\"] = ''\next[\"signing.password\"] = ''\next[\"signing.secretKeyRingFile\"] = ''\n\next[\"ossrhUsername\"] = ''\next[\"ossrhPassword\"] = ''\next[\"sonatypeStagingProfileId\"] = ''\n\next {\n\n    libraryDescription = 'This library provides the framework for cameras'\n\n    siteUrl = 'https://getbouncer.com'\n\n    scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'\n    scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'\n    scmUrl = 'https://github.com/getbouncer/cardscan-android'\n\n    licenseName = 'bouncer-free-1'\n    licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'\n\n    developerId = 'getbouncer'\n    developerName = 'Bouncer Technologies'\n    developerEmail = 'bouncer-support@stripe.com'\n\n    publishGroupId = 'com.getbouncer'\n    publishArtifactId = 'scan-camerax'\n    publishVersion = version\n}\n\ngroup = publishGroupId\nversion = publishVersion\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    Properties p = new Properties()\n    new FileInputStream(secretPropsFile).withCloseable { is ->\n        p.load(is)\n    }\n    p.each { name, value ->\n        ext[name] = value\n    }\n} else {\n    ext[\"signing.keyId\"] = System.getenv('SIGNING_KEY_ID')\n    ext[\"signing.password\"] = System.getenv('SIGNING_PASSWORD')\n    ext[\"signing.secretKeyRingFile\"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')\n\n    ext[\"ossrhUsername\"] = System.getenv('OSSRH_USERNAME')\n    ext[\"ossrhPassword\"] = System.getenv('OSSRH_PASSWORD')\n\n    ext[\"sonatypeStagingProfileId\"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')\n}\n\npublishing {\n    publications {\n        release(MavenPublication) {\n            groupId publishGroupId\n            artifactId publishArtifactId\n            version publishVersion\n\n            // Two artifacts, the `aar` (or `jar`) and the sources\n            if (project.plugins.findPlugin(\"com.android.library\")) {\n                artifact(\"$buildDir/outputs/aar/${project.getName()}-release.aar\")\n            } else {\n                artifact(\"$buildDir/libs/${project.getName()}-${version}.jar\")\n            }\n            artifact androidSourcesJar\n\n            pom {\n                name = publishArtifactId\n                description = libraryDescription\n                url = siteUrl\n                licenses {\n                    license {\n                        name = licenseName\n                        url = licenseUrl\n                    }\n                }\n                developers {\n                    developer {\n                        id = developerId\n                        name = developerName\n                        email = developerEmail\n                    }\n                }\n                scm {\n                    connection = scmConnection\n                    developerConnection = scmDeveloperConnection\n                    url = scmUrl\n                }\n                // A slightly hacky fix so that your POM will include any transitive dependencies\n                // that your library builds upon\n                withXml {\n                    def dependenciesNode = asNode().appendNode('dependencies')\n\n                    project.configurations.implementation.allDependencies.each {\n                        if (it.group != null && it.version != null) {\n                            def dependencyNode = dependenciesNode.appendNode('dependency')\n                            dependencyNode.appendNode('groupId', it.group)\n                            dependencyNode.appendNode('artifactId', it.name)\n                            dependencyNode.appendNode('version', it.version)\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // The repository to publish to, Sonatype/MavenCentral\n    repositories {\n        maven {\n            // This is an arbitrary name, you may also use \"mavencentral\" or\n            // any other name that's descriptive for you\n            name = \"sonatype\"\n            url = \"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/\"\n            credentials {\n                username ossrhUsername\n                password ossrhPassword\n            }\n        }\n    }\n}\n\nsigning {\n    sign publishing.publications\n}\n"
  },
  {
    "path": "scan-camerax/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.getbouncer.scan.camera.extension\" >\n\n    <uses-permission android:name=\"android.permission.CAMERA\" />\n    <uses-permission android:name=\"android.permission.FLASHLIGHT\" />\n\n    <uses-feature android:name=\"android.hardware.camera\" android:required=\"false\" />\n    <uses-feature android:name=\"android.hardware.camera.autofocus\" android:required=\"false\" />\n\n</manifest>\n"
  },
  {
    "path": "scan-camerax/src/main/java/com/getbouncer/scan/camera/extension/CameraAdapterImpl.kt",
    "content": "package com.getbouncer.scan.camera.extension\n\nimport android.app.Activity\nimport android.graphics.Bitmap\nimport android.graphics.PointF\nimport android.os.Build\nimport android.os.Handler\nimport android.util.DisplayMetrics\nimport android.util.Log\nimport android.util.Size\nimport android.view.ViewGroup\nimport androidx.camera.core.Camera\nimport androidx.camera.core.CameraSelector\nimport androidx.camera.core.DisplayOrientedMeteringPointFactory\nimport androidx.camera.core.FocusMeteringAction\nimport androidx.camera.core.ImageAnalysis\nimport androidx.camera.core.Preview\nimport androidx.camera.core.TorchState\nimport androidx.camera.lifecycle.ProcessCameraProvider\nimport androidx.camera.view.PreviewView\nimport androidx.core.content.ContextCompat\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.OnLifecycleEvent\nimport com.getbouncer.scan.camera.CameraAdapter\nimport com.getbouncer.scan.camera.CameraErrorListener\nimport com.getbouncer.scan.camera.CameraPreviewImage\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.Stats\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.image.getRenderScript\nimport com.getbouncer.scan.framework.image.rotate\nimport com.getbouncer.scan.framework.image.size\nimport com.getbouncer.scan.framework.util.aspectRatio\nimport com.getbouncer.scan.framework.util.centerOn\nimport com.getbouncer.scan.framework.util.minAspectRatioSurroundingSize\nimport com.getbouncer.scan.framework.util.size\nimport com.getbouncer.scan.framework.util.toRect\nimport java.util.concurrent.Executor\nimport java.util.concurrent.ExecutorService\nimport java.util.concurrent.Executors\n\ninternal class CameraAdapterImpl(\n    private val activity: Activity,\n    private val previewView: ViewGroup,\n    private val minimumResolution: Size,\n    private val cameraErrorListener: CameraErrorListener,\n) : CameraAdapter<CameraPreviewImage<Bitmap>>() {\n\n    override val implementationName: String = \"CameraX\"\n\n    private var lensFacing: Int = CameraSelector.LENS_FACING_BACK\n\n    private val mainThreadHandler = Handler(activity.mainLooper)\n\n    private var preview: Preview? = null\n    private var imageAnalyzer: ImageAnalysis? = null\n    private var camera: Camera? = null\n    private val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)\n    private lateinit var lifecycleOwner: LifecycleOwner\n\n    private val cameraListeners = mutableListOf<(Camera) -> Unit>()\n\n    /** Blocking camera operations are performed using this executor */\n    private lateinit var cameraExecutor: ExecutorService\n\n    private val display by lazy {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n            activity.display\n        } else {\n            null\n        } ?: @Suppress(\"Deprecation\") activity.windowManager.defaultDisplay\n    }\n\n    private val displayRotation by lazy { display.rotation }\n    private val displayMetrics by lazy { DisplayMetrics().also { display.getRealMetrics(it) } }\n    private val displaySize by lazy { Size(displayMetrics.widthPixels, displayMetrics.heightPixels) }\n\n    private val previewTextureView by lazy { PreviewView(activity) }\n\n    override fun withFlashSupport(task: (Boolean) -> Unit) {\n        withCamera { task(it.cameraInfo.hasFlashUnit()) }\n    }\n\n    override fun setTorchState(on: Boolean) {\n        camera?.cameraControl?.enableTorch(on)\n    }\n\n    override fun isTorchOn(): Boolean =\n        camera?.cameraInfo?.torchState?.value == TorchState.ON\n\n    override fun withSupportsMultipleCameras(task: (Boolean) -> Unit) {\n        withCameraProvider {\n            task(hasBackCamera(it) && hasFrontCamera(it))\n        }\n    }\n\n    override fun changeCamera() {\n        withCameraProvider {\n            lensFacing = when {\n                lensFacing == CameraSelector.LENS_FACING_BACK && hasFrontCamera(it) -> CameraSelector.LENS_FACING_FRONT\n                lensFacing == CameraSelector.LENS_FACING_FRONT && hasBackCamera(it) -> CameraSelector.LENS_FACING_BACK\n                hasBackCamera(it) -> CameraSelector.LENS_FACING_BACK\n                hasFrontCamera(it) -> CameraSelector.LENS_FACING_FRONT\n                else -> CameraSelector.LENS_FACING_BACK\n            }\n\n            bindCameraUseCases(it)\n        }\n    }\n\n    override fun getCurrentCamera(): Int = lensFacing\n\n    override fun setFocus(point: PointF) {\n        camera?.let { cam ->\n            val meteringPointFactory = DisplayOrientedMeteringPointFactory(\n                display,\n                cam.cameraInfo,\n                displaySize.width.toFloat(),\n                displaySize.height.toFloat(),\n            )\n            val action = FocusMeteringAction.Builder(meteringPointFactory.createPoint(point.x, point.y)).build()\n            cam.cameraControl.startFocusAndMetering(action)\n        }\n    }\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)\n    fun onCreate() {\n        // Initialize our background executor\n        cameraExecutor = Executors.newSingleThreadExecutor()\n\n        previewView.post {\n            previewView.removeAllViews()\n            previewView.addView(previewTextureView)\n\n            previewTextureView.layoutParams.apply {\n                width = ViewGroup.LayoutParams.MATCH_PARENT\n                height = ViewGroup.LayoutParams.MATCH_PARENT\n            }\n\n            previewTextureView.requestLayout()\n\n            setUpCamera()\n        }\n    }\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)\n    fun onDestroy() {\n        withCameraProvider {\n            it.unbindAll()\n            cameraExecutor.shutdown()\n        }\n    }\n\n    override fun unbindFromLifecycle(lifecycleOwner: LifecycleOwner) {\n        super.unbindFromLifecycle(lifecycleOwner)\n        withCameraProvider { cameraProvider ->\n            preview?.let { preview ->\n                cameraProvider.unbind(preview)\n            }\n        }\n    }\n\n    private fun setUpCamera() {\n        withCameraProvider {\n            lensFacing = when {\n                hasBackCamera(it) -> CameraSelector.LENS_FACING_BACK\n                hasFrontCamera(it) -> CameraSelector.LENS_FACING_FRONT\n                else -> {\n                    mainThreadHandler.post {\n                        cameraErrorListener.onCameraUnsupportedError(IllegalStateException(\"No camera is available\"))\n                    }\n                    CameraSelector.LENS_FACING_BACK\n                }\n            }\n\n            bindCameraUseCases(it)\n        }\n    }\n\n    @Synchronized\n    private fun bindCameraUseCases(cameraProvider: ProcessCameraProvider) {\n        // CameraSelector\n        val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()\n\n        preview = Preview.Builder()\n            .setTargetRotation(displayRotation)\n            .setTargetResolution(minimumResolution.resolutionToSize(displaySize))\n            .build()\n\n        imageAnalyzer = ImageAnalysis.Builder()\n            .setTargetRotation(displayRotation)\n            .setTargetResolution(minimumResolution.resolutionToSize(displaySize))\n            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)\n            .setImageQueueDepth(1)\n            .build()\n            .also { analysis ->\n                analysis.setAnalyzer(\n                    cameraExecutor\n                ) { image ->\n                    val bitmap = image.toBitmap(getRenderScript(activity))\n                        .rotate(image.imageInfo.rotationDegrees.toFloat())\n                    image.close()\n                    sendImageToStream(\n                        CameraPreviewImage(\n                            TrackedImage(bitmap, Stats.trackRepeatingTask(\"image_analysis\")),\n                            minAspectRatioSurroundingSize(\n                                previewView.size(),\n                                bitmap.size().aspectRatio()\n                            ).centerOn(displaySize.toRect())\n                        )\n                    )\n                }\n            }\n\n        cameraProvider.unbindAll()\n\n        try {\n            val newCamera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalyzer)\n            notifyCameraListeners(newCamera)\n            camera = newCamera\n\n            preview?.setSurfaceProvider(previewTextureView.surfaceProvider)\n        } catch (t: Throwable) {\n            Log.e(Config.logTag, \"Use case camera binding failed\", t)\n            mainThreadHandler.post { cameraErrorListener.onCameraOpenError(t) }\n        }\n    }\n\n    private fun notifyCameraListeners(camera: Camera) {\n        val listenerIterator = cameraListeners.iterator()\n        while (listenerIterator.hasNext()) {\n            listenerIterator.next()(camera)\n            listenerIterator.remove()\n        }\n    }\n\n    @Synchronized\n    private fun <T> withCamera(task: (Camera) -> T) {\n        val camera = this.camera\n        if (camera != null) {\n            task(camera)\n        } else {\n            cameraListeners.add { task(it) }\n        }\n    }\n\n    override fun bindToLifecycle(lifecycleOwner: LifecycleOwner) {\n        super.bindToLifecycle(lifecycleOwner)\n        this.lifecycleOwner = lifecycleOwner\n    }\n\n    /** Returns true if the device has an available back camera. False otherwise */\n    private fun hasBackCamera(cameraProvider: ProcessCameraProvider): Boolean =\n        cameraProvider.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA)\n\n    /** Returns true if the device has an available front camera. False otherwise */\n    private fun hasFrontCamera(cameraProvider: ProcessCameraProvider): Boolean =\n        cameraProvider.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA)\n\n    /**\n     * Run a task with the camera provider.\n     */\n    private fun withCameraProvider(\n        executor: Executor = ContextCompat.getMainExecutor(activity),\n        task: (ProcessCameraProvider) -> Unit,\n    ) {\n        cameraProviderFuture.addListener({ task(cameraProviderFuture.get()) }, executor)\n    }\n}\n"
  },
  {
    "path": "scan-camerax/src/main/java/com/getbouncer/scan/camera/extension/Image.kt",
    "content": "package com.getbouncer.scan.camera.extension\n\nimport android.graphics.ImageFormat\nimport android.renderscript.RenderScript\nimport androidx.annotation.CheckResult\nimport androidx.camera.core.ImageProxy\nimport com.getbouncer.scan.framework.exception.ImageTypeNotSupportedException\nimport com.getbouncer.scan.framework.image.NV21Image\nimport com.getbouncer.scan.framework.image.yuvPlanesToNV21Fast\nimport com.getbouncer.scan.framework.util.mapArray\nimport com.getbouncer.scan.framework.util.mapToIntArray\nimport com.getbouncer.scan.framework.util.toByteArray\n\n/**\n * Convert an ImageProxy to a bitmap.\n */\n@CheckResult\ninternal fun ImageProxy.toBitmap(renderScript: RenderScript) = when (format) {\n    ImageFormat.NV21 -> NV21Image(width, height, planes[0].buffer.toByteArray()).toBitmap(renderScript)\n    ImageFormat.YUV_420_888 -> NV21Image(\n        width,\n        height,\n        yuvPlanesToNV21Fast(\n            width,\n            height,\n            planes.mapArray { it.buffer },\n            planes.mapToIntArray { it.rowStride },\n            planes.mapToIntArray { it.pixelStride },\n        ),\n    ).toBitmap(renderScript)\n    else -> throw ImageTypeNotSupportedException(format)\n}\n"
  },
  {
    "path": "scan-camerax/src/main/java/com/getbouncer/scan/camera/extension/Util.kt",
    "content": "package com.getbouncer.scan.camera.extension\n\nimport android.util.Size\nimport kotlin.math.max\nimport kotlin.math.min\n\n/**\n * Convert a resolution to a size on the screen based only on the display size.\n */\ninternal fun Size.resolutionToSize(displaySize: Size) = when {\n    displaySize.width >= displaySize.height -> Size(\n        /* width */\n        max(width, height),\n        /* height */\n        min(width, height),\n    )\n    else -> Size(\n        /* width */\n        min(width, height),\n        /* height */\n        max(width, height),\n    )\n}\n"
  },
  {
    "path": "scan-framework/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "scan-framework/README.md",
    "content": "# Deprecation Notice\nHello from the Stripe (formerly Bouncer) team!\n\nWe're excited to provide an update on the state and future of the [Card Scan OCR](https://github.com/stripe/stripe-android/tree/master/stripecardscan) product! As we continue to build into Stripe's ecosystem, we'll be supporting the mission to continuously improve the end customer experience in many of Stripe's core checkout products.\n\nThis SDK has been [migrated to Stripe](https://github.com/stripe/stripe-android/tree/master/stripecardscan) and is now free for use under the MIT license!\n\nIf you are not currently a Stripe user, and interested in learning more about improving checkout experience through Stripe, please let us know and we can connect you with the team.\n\nIf you are not currently a Stripe user, and want to continue using the existing SDK, you can do so free of charge. Starting January 1, 2022, we will no longer be charging for use of the existing Bouncer Card Scan OCR SDK. For product support on [Android](https://github.com/stripe/stripe-android/issues) and [iOS](https://github.com/stripe/stripe-ios/issues). For billing support, please email [bouncer-support@stripe.com](mailto:bouncer-support@stripe.com).\n\nFor the new product, please visit the [stripe github repository](https://github.com/stripe/stripe-android/tree/master/stripecardscan).\n\n# Scan Framework\nThis repository contains the legacy, deprecated open source framework needed to quickly and accurately scan items (payment cards, IDs, etc.). [CardScan](https://cardscan.io/) is a relatively small library that provides fast and accurate payment card scanning.\n\nNote this library does not contain any user interfaces or ML models. Other libraries [Scan Payment](https://github.com/getbouncer/scan-payment-android) and [Scan UI](https://github.com/getbouncer/scan-ui-android) build upon this and add ML models and simple user interfaces. \n\nScan Framework serves as the foundation for CardScan and CardVerify enterprise libraries, which validate the authenticity of payment cards as they are scanned.\n\n![CardScan](../docs/images/demo.gif)\n\n## Contents\n* [Requirements](#requirements)\n* [Demo](#demo)\n* [Integration](#integration)\n* [Using](#using)\n* [Developing](#developing)\n* [Authors](#authors)\n* [License](#license)\n\n## Requirements\n* Android API level 21 or higher\n* Kotlin coroutine compatibility\n\nNote: Your app does not have to be written in kotlin to integrate this library, but must be able to depend on kotlin functionality.\n\n## Demo\nAn app demonstrating the basic capabilities of CardScan is available in [github](https://github.com/getbouncer/cardscan-demo-android).\n\n## Integration\nSee the [integration documentation](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) in the Bouncer Docs.\n\n## Using\nThis library is designed to be used with [scan-payment](https://github.com/getbouncer/scan-payment-android) and [scan-ui](https://github.com/getbouncer/scan-ui-android), which will provide user interfaces for scanning payment cards. However, it can be used independently.\n\nFor an overview of the architecture and design of the scan framework, see the [architecture documentation](https://docs.getbouncer.com/card-scan/android-integration-guide/android-architecture-overview).\n\n### Processing unlimited data\nLet's use an example where we process an unknown number of `MyData` values into `MyAnalyzerOutput` values, and then aggregate them into a single `MyAnalyzerOutput`.\n\nFirst, create our input and output data types:\n```kotlin\ndata class MyData(data: String)\n\ndata class MyAnalyzerOutput(output: Int)\n```\n\nNext, create an analyzer to process inputs into outputs, and a factory to create new instances of the analyzer.\n```kotlin\nclass MyAnalyzer : Analyzer<MyData, Unit, MyAnalyzerOutput> {\n    override suspend fun analyze(data: MyData, state: Unit): MyAnalyzerOutput = MyAnalyzerOutput(data.data.length)\n\n    override val name = \"my_analyzer\"\n}\n\nclass MyAnalyzerFactory : AnalyzerFactory<MyAnalyzer> {\n    override suspend fun newInstance(): Analyzer? = MyAnalyzer()\n}\n```\n\nThen, create a result handler to aggregate multiple outputs into one, and indicate when processing should cease.\n```kotlin\nclass MyResultHandler(listener: ResultHanlder<MyData, Unit, MyAnalyzerOutput>) :\n    StateUpdatingResultHandler<MyData, LoopState<Unit>, MyAnalyzerOutput>() {\n\n    private var resultsReceived = 0\n    private var totalResult = 0\n    \n    override suspend fun onResult(\n        result: MyAnalyzerOutput,\n        state: LoopState<Unit>,\n        data: MyData,\n        updateState: (LoopState<Unit>) -> Unit\n    ) {\n        resultsReceived++\n        if (resultsReceived > 10) {\n            updateState(state.copy(finished = true))\n            listener.onResult(MyAnalyzerOutput(totalResult), state.state, data)\n        } else {\n            totalResult += result.output\n        }\n    }\n}\n```\n\nFinally, tie it all together with a class that receives data and does something with the result.\n```kotlin\nclass MyDataProcessor : CoroutineScope, ResultHandler<MyData, Unit, MyAnalyzerOutput> {\n\n    private val analyzerPool = AnalyzerPool.Factory(MyAnalyzerFactory(), 4)\n    private val resultHandler = MyResultHandler(this)\n    private val loop by lazy {\n        ProcessBoundAnalyzerLoop(analyzerPool, resultHandler, Unit, \"my_loop\", { true }, { true })\n    }\n    \n    fun subscribeTo(flow: Flow<MyData>) {\n        loop.subscribeTo(flow, this)\n    }\n    \n    fun onResult(result: MyAnalyzerOutput, state: Unit, data: MyData) {\n        // Display something\n    }\n}\n```\n\n### Processing a known amount of data\nIn this example, we need to process a known amount of data as quickly as possible using multiple analyzers.\n\nFirst, create our input and output data types:\n```kotlin\ndata class MyData(data: String)\n\ndata class MyAnalyzerOutput(output: Int)\n```\n\nNext, create an analyzer to process inputs into outputs, and a factory to create new instances of the analyzer.\n```kotlin\nclass MyAnalyzer : Analyzer<MyData, Unit, MyAnalyzerOutput> {\n    override suspend fun analyze(data: MyData, state: Unit): MyAnalyzerOutput = data.data.length\n}\n\nclass MyAnalyzerFactory : AnalyzerFactory<MyAnalyzer> {\n    override fun newInstance(): Analyzer? = MyAnalyzer()\n}\n```\n\nFinally, tie it all together with a class that processes the data and does something with the results.\n```kotlin\nclass MyDataProcessor : CoroutineScope, TerminatingResultHandler<MyData, Unit, MyAnalyzerOutput> {\n\n    override val coroutineContext: CoroutineContext = Dispatchers.Default\n\n    private val analyzerFactory = MyAnalyzerFactory()\n    private val resultHandler = MyResultHandler(this)\n    private val analyzerPool = AnalyzerPool(analyzerFactory)\n\n    private val loop: AnalyzerLoop<MyData, Unit, MyAnalyzerOutput> by lazy {\n        FiniteAnalyzerLoop(\n            analyzerPool = analyzerPool,\n            resultHandler = this,\n            initialState = Unit,\n            name = \"loop_name\",\n            onAnalyzerFailure = {\n                launch(Dispatchers.Main) { analyzerFailure(it) }\n                true // terminate the loop on any analyzer failures\n            },\n            onResultFailure = {\n                launch(Dispatchers.Main) { analyzerFailure(it) }\n                true // terminate the loop on any result handler failures\n            },\n            timeLimit = 10.seconds\n        )\n    }\n    \n    fun processData(data: List<MyData>) {\n        loop.process(data, this)\n    }\n    \n    override fun onResult(result: MyAnalyzerOutput, state: Unit, data: MyData) {\n        // A single frame has been processed\n    }\n\n    override fun onAllDataProcessed() {\n        // Notify that all data has been processed\n    }\n\n    override fun onTerminatedEarly() {\n        // Notify that not all data was processed\n    }\n\n    private fun analyzerFailure(cause: Throwable?) {\n        // Notify that the data processing failed\n    }\n}\n```\n\n## Developing\nSee the [development docs](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) for details on developing this library.\n\n## Authors\nAdam Wushensky, Sam King, and Zain ul Abi Din\n\n## License\nThis library is available under the MIT license. See the [LICENSE](../LICENSE) file for the full license text.\n"
  },
  {
    "path": "scan-framework/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\napply plugin: 'kotlinx-serialization'\n\nandroid {\n    compileSdkVersion 30\n    buildToolsVersion '30.0.3'\n\n    defaultConfig {\n        minSdkVersion 21\n        targetSdkVersion 30\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles \"consumer-rules.pro\"\n\n        buildConfigField(\"String\", \"SDK_VERSION_STRING\", \"\\\"${version}\\\"\")\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile(\"proguard-android-optimize.txt\"), \"proguard-rules.pro\"\n        }\n    }\n\n    testOptions {\n        unitTests.includeAndroidResources = true\n    }\n\n    lintOptions {\n        enable \"Interoperability\"\n    }\n\n    packagingOptions {\n        pickFirst 'META-INF/AL2.0'\n        pickFirst 'META-INF/LGPL2.1'\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = JavaVersion.VERSION_1_8.toString()\n    }\n\n    aaptOptions {\n        noCompress \"tflite\"\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: \"libs\", include: [\"*.jar\"])\n\n    implementation \"androidx.core:core-ktx:[1.3.1,1.6.0]\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-core:[1.4.0,1.5.1]\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-android:[1.4.0,1.5.1]\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-serialization-json:[1.1.0,1.2.2]\"\n\n    // Allow the user to specify their own version of Tensorflow Lite to include\n    runtimeOnly project(\":tensorflow-lite\")\n    compileOnly \"org.tensorflow:tensorflow-lite:2.4.0\"\n}\n\ndependencies {\n    testImplementation \"androidx.test:core:1.4.0\"\n    testImplementation \"androidx.test:runner:1.4.0\"\n    testImplementation \"junit:junit:4.13.2\"\n    testImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n    testImplementation \"org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1\"\n}\n\ndependencies {\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.4.0\"\n    androidTestImplementation \"androidx.test.ext:junit:1.1.3\"\n    androidTestImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n    androidTestImplementation \"org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1\"\n}\n\napply from: \"deploy.gradle\"\n"
  },
  {
    "path": "scan-framework/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "scan-framework/deploy.gradle",
    "content": "apply plugin: 'maven-publish'\napply plugin: 'org.jetbrains.dokka'\napply plugin: 'signing'\n\ntask androidSourcesJar(type: Jar) {\n    archiveClassifier.set('sources')\n    if (project.plugins.findPlugin(\"com.android.library\")) {\n        // Android library\n        from android.sourceSets.main.java.srcDirs\n        from android.sourceSets.main.kotlin.srcDirs\n    } else {\n        // Pure kotlin library\n        from sourceSets.main.java.srcDirs\n        from sourceSets.main.kotlin.srcDirs\n    }\n}\n\ntasks.withType(dokkaHtmlPartial.getClass()).configureEach {\n    pluginsMapConfiguration.set(\n        [\"org.jetbrains.dokka.base.DokkaBase\": \"\"\"{ \"separateInheritedMembers\": true}\"\"\"]\n    )\n}\n\ntask javadocJar(type: Jar, dependsOn: dokkaJavadoc) {\n    archiveClassifier.set('javadoc')\n    from dokkaJavadoc.outputDirectory\n}\n\nartifacts {\n    archives androidSourcesJar\n    archives javadocJar\n}\n\next[\"signing.keyId\"] = ''\next[\"signing.password\"] = ''\next[\"signing.secretKeyRingFile\"] = ''\n\next[\"ossrhUsername\"] = ''\next[\"ossrhPassword\"] = ''\next[\"sonatypeStagingProfileId\"] = ''\n\next {\n\n    libraryDescription = 'This library provides the framework for scanning'\n\n    siteUrl = 'https://getbouncer.com'\n\n    scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'\n    scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'\n    scmUrl = 'https://github.com/getbouncer/cardscan-android'\n\n    licenseName = 'bouncer-free-1'\n    licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'\n\n    developerId = 'getbouncer'\n    developerName = 'Bouncer Technologies'\n    developerEmail = 'bouncer-support@stripe.com'\n\n    publishGroupId = 'com.getbouncer'\n    publishArtifactId = 'scan-framework'\n    publishVersion = version\n}\n\ngroup = publishGroupId\nversion = publishVersion\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    Properties p = new Properties()\n    new FileInputStream(secretPropsFile).withCloseable { is ->\n        p.load(is)\n    }\n    p.each { name, value ->\n        ext[name] = value\n    }\n} else {\n    ext[\"signing.keyId\"] = System.getenv('SIGNING_KEY_ID')\n    ext[\"signing.password\"] = System.getenv('SIGNING_PASSWORD')\n    ext[\"signing.secretKeyRingFile\"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')\n\n    ext[\"ossrhUsername\"] = System.getenv('OSSRH_USERNAME')\n    ext[\"ossrhPassword\"] = System.getenv('OSSRH_PASSWORD')\n\n    ext[\"sonatypeStagingProfileId\"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')\n}\n\npublishing {\n    publications {\n        release(MavenPublication) {\n            groupId publishGroupId\n            artifactId publishArtifactId\n            version publishVersion\n\n            // Two artifacts, the `aar` (or `jar`) and the sources\n            if (project.plugins.findPlugin(\"com.android.library\")) {\n                artifact(\"$buildDir/outputs/aar/${project.getName()}-release.aar\")\n            } else {\n                artifact(\"$buildDir/libs/${project.getName()}-${version}.jar\")\n            }\n            artifact androidSourcesJar\n\n            pom {\n                name = publishArtifactId\n                description = libraryDescription\n                url = siteUrl\n                licenses {\n                    license {\n                        name = licenseName\n                        url = licenseUrl\n                    }\n                }\n                developers {\n                    developer {\n                        id = developerId\n                        name = developerName\n                        email = developerEmail\n                    }\n                }\n                scm {\n                    connection = scmConnection\n                    developerConnection = scmDeveloperConnection\n                    url = scmUrl\n                }\n                // A slightly hacky fix so that your POM will include any transitive dependencies\n                // that your library builds upon\n                withXml {\n                    def dependenciesNode = asNode().appendNode('dependencies')\n\n                    project.configurations.implementation.allDependencies.each {\n                        if (it.group != null && it.version != null) {\n                            def dependencyNode = dependenciesNode.appendNode('dependency')\n                            dependencyNode.appendNode('groupId', it.group)\n                            dependencyNode.appendNode('artifactId', it.name)\n                            dependencyNode.appendNode('version', it.version)\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // The repository to publish to, Sonatype/MavenCentral\n    repositories {\n        maven {\n            // This is an arbitrary name, you may also use \"mavencentral\" or\n            // any other name that's descriptive for you\n            name = \"sonatype\"\n            url = \"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/\"\n            credentials {\n                username ossrhUsername\n                password ossrhPassword\n            }\n        }\n    }\n}\n\nsigning {\n    sign publishing.publications\n}\n"
  },
  {
    "path": "scan-framework/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\n-keep class org.tensorflow.lite.Interpreter { *; }\n"
  },
  {
    "path": "scan-framework/src/androidTest/assets/sample_resource.tflite",
    "content": "ABC123\nDEF456\n"
  },
  {
    "path": "scan-framework/src/androidTest/java/com/getbouncer/scan/framework/FetcherTest.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport androidx.test.filters.LargeTest\nimport androidx.test.filters.SmallTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.test.runBlockingTest\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\nimport java.net.URL\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertNull\nimport kotlin.test.assertTrue\n\nclass FetcherTest {\n    private val testContext = InstrumentationRegistry.getInstrumentation().context\n\n    @Before\n    fun before() {\n        Config.apiKey = \"qOJ_fF-WLDMbG05iBq5wvwiTNTmM2qIn\"\n    }\n\n    @After\n    fun after() {\n        Config.apiKey = null\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun fetchResource_success() = runBlockingTest {\n        class ResourceFetcherImpl : ResourceFetcher() {\n            override val assetFileName: String = \"sample_resource.tflite\"\n            override val modelVersion: String = \"sample_resource\"\n            override val hash: String = \"0dcf3e387c68dfea8dd72a183f1f765478ebaa4d8544cfc09a16e87a795d8ccf\"\n            override val hashAlgorithm: String = \"SHA-256\"\n            override val modelClass: String = \"sample_class\"\n            override val modelFrameworkVersion: Int = 2049\n        }\n\n        assertEquals(\n            expected = FetchedResource(\n                modelClass = \"sample_class\",\n                modelFrameworkVersion = 2049,\n                modelVersion = \"sample_resource\",\n                modelHash = \"0dcf3e387c68dfea8dd72a183f1f765478ebaa4d8544cfc09a16e87a795d8ccf\",\n                modelHashAlgorithm = \"SHA-256\",\n                assetFileName = \"sample_resource.tflite\",\n            ),\n            actual = ResourceFetcherImpl().fetchData(forImmediateUse = false, isOptional = false)\n        )\n    }\n\n    @Test\n    @LargeTest\n    fun fetchModelFromWebDirectly_success() = runBlocking {\n        class FetcherImpl : DirectDownloadWebFetcher(testContext) {\n            override val url = URL(\"https://downloads.getbouncer.com/ocr/darknite/android/darknite.tflite\")\n            override val hash = \"0ef6e590a5c8b0da63546079a0afacd8ccb72418af68972b72fda45deaca543a\"\n            override val hashAlgorithm = \"SHA-256\"\n            override val modelVersion = \"darknite\"\n            override val modelClass = \"ocr\"\n            override val modelFrameworkVersion = 1\n        }\n\n        // force downloading the model for this test\n        val fetcher = FetcherImpl()\n        fetcher.clearCache()\n\n        val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)\n        assertTrue { fetchedModel is FetchedFile }\n\n        val file = (fetchedModel as FetchedFile).file\n        assertNotNull(file)\n\n        val reader = file.reader()\n        reader.skip(4)\n        assertEquals('T', reader.read().toChar())\n        assertEquals('F', reader.read().toChar())\n        assertEquals('L', reader.read().toChar())\n        assertEquals('3', reader.read().toChar())\n    }\n\n    @Test\n    @LargeTest\n    fun fetchModelFromWebSignedUrl_success() = runBlocking {\n        class FetcherImpl : SignedUrlModelWebFetcher(testContext) {\n            override val modelClass = \"four_recognize\"\n            override val modelFrameworkVersion = 1\n            override val modelVersion = \"0.0.1.16\"\n            override val modelFileName = \"fourrecognize.tflite\"\n            override val hash = \"55eea0d57239a7e92904fb15209963f7236bd06919275bdeb0a765a94b559c97\"\n            override val hashAlgorithm = \"SHA-256\"\n        }\n\n        // force downloading the model for this test\n        val fetcher = FetcherImpl()\n        fetcher.clearCache()\n\n        val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)\n        assertTrue { fetchedModel is FetchedFile }\n\n        val file = (fetchedModel as FetchedFile).file\n        assertNotNull(file)\n\n        val reader = file.reader()\n        reader.skip(4)\n        assertEquals('T', reader.read().toChar())\n        assertEquals('F', reader.read().toChar())\n        assertEquals('L', reader.read().toChar())\n        assertEquals('3', reader.read().toChar())\n    }\n\n    @Test\n    @LargeTest\n    fun fetchModelFromWebSignedUrl_downloadFail() = runBlocking {\n        class FetcherImpl : SignedUrlModelWebFetcher(testContext) {\n            override val modelClass = \"invalid_model\"\n            override val modelFrameworkVersion = 1\n            override val modelVersion = \"0.0.1.16\"\n            override val modelFileName = \"fourrecognize.tflite\"\n            override val hash = \"55eea0d57239a7e92904fb15209963f7236bd06919275bdeb0a765a94b559c97\"\n            override val hashAlgorithm = \"SHA-256\"\n        }\n\n        // force downloading the model for this test\n        val fetcher = FetcherImpl()\n        fetcher.clearCache()\n\n        val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)\n        assertTrue { fetchedModel is FetchedFile }\n\n        assertNull((fetchedModel as FetchedFile).file)\n    }\n\n    @Test\n    @LargeTest\n    fun fetchUpgradableModelFromWeb_success() = runBlocking {\n        class FetcherImpl : UpdatingModelWebFetcher(testContext) {\n            override val modelClass = \"four_recognize\"\n            override val modelFrameworkVersion = 1\n            override val defaultModelVersion = \"0.0.1.16\"\n            override val defaultModelFileName = \"fourrecognize.tflite\"\n            override val defaultModelHash = \"abc\"\n            override val defaultModelHashAlgorithm = \"SHA-256\"\n        }\n\n        // force downloading the model for this test\n        val fetcher = FetcherImpl()\n        fetcher.clearCache()\n\n        val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)\n        assertTrue { fetchedModel is FetchedFile }\n\n        val file = (fetchedModel as FetchedFile).file\n        assertNotNull(file)\n\n        val reader = file.reader()\n        reader.skip(4)\n        assertEquals('T', reader.read().toChar())\n        assertEquals('F', reader.read().toChar())\n        assertEquals('L', reader.read().toChar())\n        assertEquals('3', reader.read().toChar())\n    }\n\n    @Test\n    @LargeTest\n    fun fetchUpgradableModelFromWeb_fallbackSuccess() = runBlocking {\n        class FetcherImpl : UpdatingModelWebFetcher(testContext) {\n            override val modelClass = \"four_recognize\"\n            override val modelFrameworkVersion = 2049\n            override val defaultModelVersion = \"0.0.1.16\"\n            override val defaultModelFileName = \"fourrecognize.tflite\"\n            override val defaultModelHash = \"55eea0d57239a7e92904fb15209963f7236bd06919275bdeb0a765a94b559c97\"\n            override val defaultModelHashAlgorithm = \"SHA-256\"\n        }\n\n        // force downloading the model for this test\n        val fetcher = FetcherImpl()\n        fetcher.clearCache()\n\n        val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)\n        assertTrue { fetchedModel is FetchedFile }\n\n        val file = (fetchedModel as FetchedFile).file\n        assertNotNull(file)\n\n        val reader = file.reader()\n        reader.skip(4)\n        assertEquals('T', reader.read().toChar())\n        assertEquals('F', reader.read().toChar())\n        assertEquals('L', reader.read().toChar())\n        assertEquals('3', reader.read().toChar())\n    }\n\n    @Test\n    @LargeTest\n    fun fetchUpgradableModelFromWeb_successForImmediateUse() = runBlocking {\n        class FetcherImpl : UpdatingModelWebFetcher(testContext) {\n            override val modelClass = \"four_recognize\"\n            override val modelFrameworkVersion = 2049\n            override val defaultModelVersion = \"0.0.1.16\"\n            override val defaultModelFileName = \"fourrecognize.tflite\"\n            override val defaultModelHash = \"55eea0d57239a7e92904fb15209963f7236bd06919275bdeb0a765a94b559c97\"\n            override val defaultModelHashAlgorithm = \"SHA-256\"\n        }\n\n        // force downloading the model for this test\n        val fetcher = FetcherImpl()\n        fetcher.clearCache()\n\n        val fetchedModel = fetcher.fetchData(forImmediateUse = true, isOptional = false)\n        assertTrue { fetchedModel is FetchedFile }\n\n        val file = (fetchedModel as FetchedFile).file\n        assertNotNull(file)\n\n        val reader = file.reader()\n        reader.skip(4)\n        assertEquals('T', reader.read().toChar())\n        assertEquals('F', reader.read().toChar())\n        assertEquals('L', reader.read().toChar())\n        assertEquals('3', reader.read().toChar())\n    }\n\n    @Test\n    @LargeTest\n    fun fetchUpgradableModelFromWeb_fail() = runBlocking {\n        class FetcherImpl : UpdatingModelWebFetcher(testContext) {\n            override val modelClass = \"four_recognize\"\n            override val modelFrameworkVersion = 2049\n            override val defaultModelVersion = \"0.0.1.16\"\n            override val defaultModelFileName = \"fourrecognize.tflite\"\n            override val defaultModelHash = \"abc\"\n            override val defaultModelHashAlgorithm = \"SHA-256\"\n        }\n\n        // force downloading the model for this test\n        val fetcher = FetcherImpl()\n        fetcher.clearCache()\n\n        val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)\n        assertTrue { fetchedModel is FetchedFile }\n\n        assertNull((fetchedModel as FetchedFile).file)\n    }\n\n    @Test\n    @LargeTest\n    fun fetchUpgradableResourceModel_success() = runBlocking {\n        class FetcherImpl : UpdatingResourceFetcher(testContext) {\n            override val assetFileName: String = \"sample_resource.tflite\"\n            override val resourceModelVersion = \"demo\"\n            override val resourceModelHash = \"0dcf3e387c68dfea8dd72a183f1f765478ebaa4d8544cfc09a16e87a795d8ccf\"\n            override val resourceModelHashAlgorithm = \"SHA-256\"\n            override val modelClass = \"four_recognize\"\n            override val modelFrameworkVersion = 1\n        }\n\n        // force downloading the model for this test\n        val fetcher = FetcherImpl()\n        fetcher.clearCache()\n\n        val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)\n        assertTrue(\"fetchedModel is $fetchedModel\") { fetchedModel is FetchedFile }\n\n        val file = (fetchedModel as FetchedFile).file\n        assertNotNull(file)\n\n        val reader = file.reader()\n        reader.skip(4)\n        assertEquals('T', reader.read().toChar())\n        assertEquals('F', reader.read().toChar())\n        assertEquals('L', reader.read().toChar())\n        assertEquals('3', reader.read().toChar())\n    }\n\n    @Test\n    @LargeTest\n    fun fetchUpgradableResourceModel_successForImmediateUse() = runBlocking {\n        class FetcherImpl : UpdatingResourceFetcher(testContext) {\n            override val assetFileName: String = \"sample_resource.tflite\"\n            override val resourceModelVersion = \"demo\"\n            override val resourceModelHash = \"0dcf3e387c68dfea8dd72a183f1f765478ebaa4d8544cfc09a16e87a795d8ccf\"\n            override val resourceModelHashAlgorithm = \"SHA-256\"\n            override val modelClass = \"four_recognize\"\n            override val modelFrameworkVersion = 1\n        }\n\n        // force downloading the model for this test\n        val fetcher = FetcherImpl()\n        fetcher.clearCache()\n\n        val fetchedModel = fetcher.fetchData(forImmediateUse = true, isOptional = false)\n        assertTrue { fetchedModel is FetchedResource }\n\n        assertEquals(\"sample_resource.tflite\", (fetchedModel as FetchedResource).assetFileName)\n    }\n\n    @Test\n    @LargeTest\n    fun fetchUpgradableResourceModel_downloadFail() = runBlocking {\n        class FetcherImpl : UpdatingResourceFetcher(testContext) {\n            override val assetFileName: String = \"sample_resource.tflite\"\n            override val resourceModelVersion = \"demo\"\n            override val resourceModelHash = \"0dcf3e387c68dfea8dd72a183f1f765478ebaa4d8544cfc09a16e87a795d8ccf\"\n            override val resourceModelHashAlgorithm = \"SHA-256\"\n            override val modelClass = \"invalid_model_class\"\n            override val modelFrameworkVersion = 1\n        }\n\n        // force downloading the model for this test\n        val fetcher = FetcherImpl()\n        fetcher.clearCache()\n\n        val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)\n        assertTrue { fetchedModel is FetchedResource }\n\n        assertEquals(\"sample_resource.tflite\", (fetchedModel as FetchedResource).assetFileName)\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/androidTest/java/com/getbouncer/scan/framework/LoaderTest.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport androidx.test.filters.SmallTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport kotlinx.coroutines.runBlocking\nimport org.junit.Test\nimport java.io.File\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertTrue\n\nclass LoaderTest {\n    private val testContext = InstrumentationRegistry.getInstrumentation().context\n\n    @Test\n    @SmallTest\n    fun loadData_fromResource_success() = runBlocking {\n        val fetchedData = FetchedResource(\n            modelClass = \"sample_class\",\n            modelFrameworkVersion = 2049,\n            modelVersion = \"sample_resource\",\n            modelHash = \"0dcf3e387c68dfea8dd72a183f1f765478ebaa4d8544cfc09a16e87a795d8ccf\",\n            modelHashAlgorithm = \"SHA-256\",\n            assetFileName = \"sample_resource.tflite\",\n        )\n\n        val byteBuffer = Loader(testContext).loadData(fetchedData)\n        assertNotNull(byteBuffer)\n        assertEquals(14, byteBuffer.limit(), \"File is not expected size\")\n        byteBuffer.rewind()\n\n        // ensure not all bytes are zero\n        var encounteredNonZeroByte = false\n        while (!encounteredNonZeroByte) {\n            encounteredNonZeroByte = byteBuffer.get().toInt() != 0\n        }\n        assertTrue(encounteredNonZeroByte, \"All bytes were zero\")\n\n        // ensure bytes are correct\n        byteBuffer.rewind()\n        assertEquals('A', byteBuffer.get().toInt().toChar())\n        assertEquals('B', byteBuffer.get().toInt().toChar())\n        assertEquals('C', byteBuffer.get().toInt().toChar())\n        assertEquals('1', byteBuffer.get().toInt().toChar())\n    }\n\n    @Test\n    @SmallTest\n    fun loadData_fromFile_success() = runBlocking {\n        val sampleFile = File(testContext.cacheDir, \"sample_file\")\n        if (sampleFile.exists()) {\n            sampleFile.delete()\n        }\n\n        sampleFile.createNewFile()\n        sampleFile.writeText(\"ABC123\")\n\n        val fetchedData = FetchedFile(\n            modelClass = \"sample_class\",\n            modelFrameworkVersion = 2049,\n            modelVersion = \"sample_file\",\n            modelHash = \"133351546614bfadfa68bb66c22a06265972b02791e4ac545ad900f20fe1a796\",\n            modelHashAlgorithm = \"SHA-256\",\n            file = sampleFile,\n        )\n\n        val byteBuffer = Loader(testContext).loadData(fetchedData)\n        assertNotNull(byteBuffer)\n        assertEquals(6, byteBuffer.limit(), \"File is not expected size\")\n        byteBuffer.rewind()\n\n        // ensure not all bytes are zero\n        var encounteredNonZeroByte = false\n        while (!encounteredNonZeroByte) {\n            encounteredNonZeroByte = byteBuffer.get().toInt() != 0\n        }\n        assertTrue(encounteredNonZeroByte, \"All bytes were zero\")\n\n        // ensure bytes are correct\n        byteBuffer.rewind()\n        assertEquals('A', byteBuffer.get().toInt().toChar())\n        assertEquals('B', byteBuffer.get().toInt().toChar())\n        assertEquals('C', byteBuffer.get().toInt().toChar())\n        assertEquals('1', byteBuffer.get().toInt().toChar())\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/androidTest/java/com/getbouncer/scan/framework/StorageTest.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport androidx.test.filters.SmallTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport org.junit.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertTrue\n\nprivate const val PURPOSE_TEST = \"test\"\n\nclass StorageTest {\n    private val testContext = InstrumentationRegistry.getInstrumentation().context\n\n    @Test\n    @SmallTest\n    fun storeAndRetrieveString() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertTrue(storage.storeValue(key, \"test_string\"))\n        assertEquals(\"test_string\", storage.getString(key, \"wrong\"))\n    }\n\n    @Test\n    @SmallTest\n    fun storeAndRetrieveLong() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertTrue(storage.storeValue(key, 1L))\n        assertEquals(1L, storage.getLong(key, 2L))\n    }\n\n    @Test\n    @SmallTest\n    fun storeAndRetrieveInt() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertTrue(storage.storeValue(key, 1))\n        assertEquals(1, storage.getInt(key, 2))\n    }\n\n    @Test\n    @SmallTest\n    fun storeAndRetrieveFloat() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertTrue(storage.storeValue(key, 1F))\n        assertEquals(1F, storage.getFloat(key, 2F))\n    }\n\n    @Test\n    @SmallTest\n    fun storeAndRetrieveBoolean() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertTrue(storage.storeValue(key, true))\n        assertEquals(true, storage.getBoolean(key, false))\n    }\n\n    @Test\n    @SmallTest\n    fun retrieveMissingString() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertEquals(\"default\", storage.getString(key, \"default\"))\n    }\n\n    @Test\n    @SmallTest\n    fun retrieveMissingLong() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertEquals(1L, storage.getLong(key, 1L))\n    }\n\n    @Test\n    @SmallTest\n    fun retrieveMissingInt() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertEquals(1, storage.getInt(key, 1))\n    }\n\n    @Test\n    @SmallTest\n    fun retrieveMissingFloat() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertEquals(1F, storage.getFloat(key, 1F))\n    }\n\n    @Test\n    @SmallTest\n    fun retrieveMissingBoolean() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertEquals(true, storage.getBoolean(key, true))\n    }\n\n    @Test\n    @SmallTest\n    fun retrieveWrongTypeString() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertTrue(storage.storeValue(key, 1L))\n        assertEquals(\"default\", storage.getString(key, \"default\"))\n    }\n\n    @Test\n    @SmallTest\n    fun retrieveWrongTypeLong() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertTrue(storage.storeValue(key, 1F))\n        assertEquals(1L, storage.getLong(key, 1L))\n    }\n\n    @Test\n    @SmallTest\n    fun retrieveWrongTypeInt() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertTrue(storage.storeValue(key, 1F))\n        assertEquals(1, storage.getInt(key, 1))\n    }\n\n    @Test\n    @SmallTest\n    fun retrieveWrongTypeFloat() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertTrue(storage.storeValue(key, true))\n        assertEquals(1F, storage.getFloat(key, 1F))\n    }\n\n    @Test\n    @SmallTest\n    fun retrieveWrongTypeBoolean() {\n        val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)\n        val key = \"test\"\n        assertTrue(storage.storeValue(key, \"test_value\"))\n        assertEquals(true, storage.getBoolean(key, true))\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/androidTest/java/com/getbouncer/scan/framework/api/BouncerApiTest.kt",
    "content": "package com.getbouncer.scan.framework.api\n\nimport androidx.test.filters.LargeTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.Stats\nimport com.getbouncer.scan.framework.api.dto.AppInfo\nimport com.getbouncer.scan.framework.api.dto.BouncerErrorResponse\nimport com.getbouncer.scan.framework.api.dto.ClientDevice\nimport com.getbouncer.scan.framework.api.dto.ModelVersion\nimport com.getbouncer.scan.framework.api.dto.ScanStatistics\nimport com.getbouncer.scan.framework.api.dto.StatsPayload\nimport com.getbouncer.scan.framework.ml.getLoadedModelVersions\nimport com.getbouncer.scan.framework.ml.trackModelLoaded\nimport com.getbouncer.scan.framework.util.AppDetails\nimport com.getbouncer.scan.framework.util.Device\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.test.runBlockingTest\nimport kotlinx.serialization.Serializable\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNotEquals\nimport kotlin.test.assertNotNull\nimport kotlin.test.fail\n\nclass BouncerApiTest {\n\n    companion object {\n        private const val STATS_PATH = \"/scan_stats\"\n    }\n\n    private val testContext = InstrumentationRegistry.getInstrumentation().context\n    private val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n\n    @Before\n    fun before() {\n        Config.apiKey = \"qOJ_fF-WLDMbG05iBq5wvwiTNTmM2qIn\"\n    }\n\n    @After\n    fun after() {\n        Config.apiKey = null\n    }\n\n    @Test\n    @LargeTest\n    @ExperimentalCoroutinesApi\n    fun uploadScanStats_success() = runBlockingTest {\n        for (i in 0..100) {\n            Stats.trackRepeatingTask(\"test_repeating_task_1\").trackResult(\"$i\")\n        }\n\n        for (i in 0..100) {\n            Stats.trackRepeatingTask(\"test_repeating_task_2\").trackResult(\"$i\")\n        }\n\n        val task1 = Stats.trackTask(\"test_task_1\")\n        for (i in 0..5) {\n            task1.trackResult(\"$i\")\n        }\n\n        trackModelLoaded(\"test_model_class\", \"test_model_vesion\", 2, true)\n\n        when (\n            val result = postForResult(\n                context = appContext,\n                path = STATS_PATH,\n                data = StatsPayload(\n                    instanceId = \"test_instance_id\",\n                    scanId = \"test_scan_id\",\n                    device = ClientDevice.fromDevice(Device.fromContext(testContext)),\n                    app = AppInfo.fromAppDetails(AppDetails.fromContext(testContext)),\n                    scanStats = ScanStatistics.fromStats(),\n                    modelVersions = getLoadedModelVersions().map { ModelVersion.fromModelLoadDetails(it) },\n                ),\n                requestSerializer = StatsPayload.serializer(),\n                responseSerializer = ScanStatsResults.serializer(),\n                errorSerializer = BouncerErrorResponse.serializer()\n            )\n        ) {\n            is NetworkResult.Success -> {\n                assertEquals(200, result.responseCode)\n            }\n            else -> fail(\"Network result was not success: $result\")\n        }\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @LargeTest\n    fun getModelSignedUrl() = runBlocking {\n        when (\n            val result = getModelSignedUrl(\n                appContext,\n                \"four_recognize\",\n                \"v0.0.1\",\n                \"model.tflite\"\n            )\n        ) {\n            is NetworkResult.Success -> {\n                assertNotNull(result.body.modelUrl)\n                assertNotEquals(\"\", result.body.modelUrl)\n            }\n            else -> fail(\"network result was not success: $result\")\n        }\n    }\n\n    @Serializable\n    data class ScanStatsResults(val status: String? = \"\")\n}\n"
  },
  {
    "path": "scan-framework/src/androidTest/java/com/getbouncer/scan/framework/image/BitmapExtensionsTest.kt",
    "content": "package com.getbouncer.scan.framework.image\n\nimport android.graphics.Rect\nimport android.util.Size\nimport androidx.core.graphics.drawable.toBitmap\nimport androidx.test.filters.SmallTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.getbouncer.scan.framework.test.R\nimport org.junit.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertTrue\n\nclass BitmapExtensionsTest {\n\n    private val testResources = InstrumentationRegistry.getInstrumentation().context.resources\n\n    @Test\n    @SmallTest\n    fun bitmap_scale_isCorrect() {\n        // read in a sample bitmap file\n        val bitmap = testResources.getDrawable(R.drawable.ocr_card_numbers_clear, null).toBitmap()\n        assertNotNull(bitmap)\n        assertEquals(600, bitmap.width, \"Bitmap width is not expected\")\n        assertEquals(375, bitmap.height, \"Bitmap height is not expected\")\n\n        // scale the bitmap\n        val scaledBitmap = bitmap.scale(0.2F)\n\n        // check the expected sizes of the images\n        assertEquals(\n            Size(bitmap.width / 5, bitmap.height / 5),\n            Size(scaledBitmap.width, scaledBitmap.height),\n            \"Scaled image is the wrong size\"\n        )\n\n        // check each pixel of the images\n        var encounteredNonZeroPixel = false\n        for (x in 0 until scaledBitmap.width) {\n            for (y in 0 until scaledBitmap.height) {\n                encounteredNonZeroPixel = encounteredNonZeroPixel || scaledBitmap.getPixel(x, y) != 0\n            }\n        }\n\n        assertTrue(encounteredNonZeroPixel, \"Pixels were all zero\")\n    }\n\n    @Test\n    @SmallTest\n    fun bitmap_crop_isCorrect() {\n        val bitmap = testResources.getDrawable(R.drawable.ocr_card_numbers_clear, null).toBitmap()\n        assertNotNull(bitmap)\n        assertEquals(600, bitmap.width, \"Bitmap width is not expected\")\n        assertEquals(375, bitmap.height, \"Bitmap height is not expected\")\n\n        // crop the bitmap\n        val croppedBitmap = bitmap.crop(\n            Rect(\n                bitmap.width / 4,\n                bitmap.height / 4,\n                bitmap.width * 3 / 4,\n                bitmap.height * 3 / 4\n            )\n        )\n\n        // check the expected sizes of the images\n        assertEquals(\n            Size(bitmap.width * 3 / 4 - bitmap.width / 4, bitmap.height * 3 / 4 - bitmap.height / 4),\n            Size(croppedBitmap.width, croppedBitmap.height),\n            \"Cropped image is the wrong size\"\n        )\n\n        // check each pixel of the images\n        var encounteredNonZeroPixel = false\n        for (x in 0 until croppedBitmap.width) {\n            for (y in 0 until croppedBitmap.height) {\n                val croppedPixel = croppedBitmap.getPixel(x, y)\n                val originalPixel = bitmap.getPixel(x + bitmap.width / 4, y + bitmap.height / 4)\n                assertEquals(originalPixel, croppedPixel, \"Difference at pixel $x, $y\")\n                encounteredNonZeroPixel = encounteredNonZeroPixel || croppedPixel != 0\n            }\n        }\n\n        assertTrue(encounteredNonZeroPixel, \"Pixels were all zero\")\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/androidTest/java/com/getbouncer/scan/framework/layout/LayoutTest.kt",
    "content": "package com.getbouncer.scan.framework.layout\n\nimport android.graphics.Rect\nimport android.util.Size\nimport androidx.test.filters.SmallTest\nimport com.getbouncer.scan.framework.util.adjustSizeToAspectRatio\nimport com.getbouncer.scan.framework.util.centerScaled\nimport com.getbouncer.scan.framework.util.intersectionWith\nimport com.getbouncer.scan.framework.util.maxAspectRatioInSize\nimport com.getbouncer.scan.framework.util.minAspectRatioSurroundingSize\nimport com.getbouncer.scan.framework.util.move\nimport com.getbouncer.scan.framework.util.projectRegionOfInterest\nimport com.getbouncer.scan.framework.util.scaleAndCenterWithin\nimport org.junit.Test\nimport java.lang.IllegalArgumentException\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFailsWith\n\nclass LayoutTest {\n\n    @Test\n    @SmallTest\n    fun maxAspectRatioInSize_sameRatio() {\n        // the same aspect ratio as the size\n        assertEquals(Size(16, 9), maxAspectRatioInSize(Size(16, 9), 16.toFloat() / 9))\n    }\n\n    @Test\n    @SmallTest\n    fun maxAspectRatioInSize_wide() {\n        // an aspect ratio that's wider than tall\n        assertEquals(Size(16, 9), maxAspectRatioInSize(Size(16, 16), 16.toFloat() / 9))\n    }\n\n    @Test\n    @SmallTest\n    fun maxAspectRatioInSize_tall() {\n        // an aspect ratio that's taller than wide\n        assertEquals(Size(9, 16), maxAspectRatioInSize(Size(16, 16), 9.toFloat() / 16))\n    }\n\n    @Test\n    @SmallTest\n    fun scaleAndCenterWithin_horizontal() {\n        // center horizontally\n        assertEquals(Rect(5, 0, 20, 15), Size(4, 4).scaleAndCenterWithin(Size(25, 15)))\n    }\n\n    @Test\n    @SmallTest\n    fun scaleAndCenterWithin_vertical() {\n        // center vertically\n        assertEquals(Rect(0, 5, 15, 20), Size(4, 4).scaleAndCenterWithin(Size(15, 25)))\n    }\n\n    @Test\n    @SmallTest\n    fun scaleAndCenterWithin_sameSquare() {\n        // same ratio\n        assertEquals(Rect(0, 0, 15, 15), Size(4, 4).scaleAndCenterWithin(Size(15, 15)))\n    }\n\n    @Test\n    @SmallTest\n    fun scaleAndCenterWithin_sameRectangle() {\n        // same ratio, not square\n        assertEquals(Rect(0, 0, 25, 15), Size(5, 3).scaleAndCenterWithin(Size(25, 15)))\n    }\n\n    @Test\n    @SmallTest\n    fun centerScaled_horizontal() {\n        assertEquals(Rect(0, 0, 16, 8), Rect(4, 0, 12, 8).centerScaled(2F, 1F))\n    }\n\n    @Test\n    @SmallTest\n    fun centerScaled_vertical() {\n        assertEquals(Rect(0, 0, 8, 16), Rect(0, 4, 8, 12).centerScaled(1F, 2F))\n    }\n\n    @Test\n    @SmallTest\n    fun centerScaled_sameSquare() {\n        assertEquals(Rect(0, 0, 16, 16), Rect(4, 4, 12, 12).centerScaled(2F, 2F))\n    }\n\n    @Test\n    @SmallTest\n    fun centerScaled_sameRectangle() {\n        assertEquals(Rect(0, 0, 8, 16), Rect(2, 4, 6, 12).centerScaled(2F, 2F))\n    }\n\n    @Test\n    @SmallTest\n    fun intersectionWith_sameRectangle() {\n        assertEquals(Rect(0, 0, 15, 15), Rect(0, 0, 15, 15).intersectionWith(Rect(0, 0, 15, 15)))\n    }\n\n    @Test\n    @SmallTest\n    fun intersectionWith_parent() {\n        assertEquals(Rect(2, 2, 15, 15), Rect(2, 2, 15, 15).intersectionWith(Rect(0, 0, 17, 17)))\n    }\n\n    @Test\n    @SmallTest\n    fun intersectionWith_child() {\n        assertEquals(Rect(2, 2, 15, 15), Rect(0, 0, 17, 17).intersectionWith(Rect(2, 2, 15, 15)))\n    }\n\n    @Test\n    @SmallTest\n    fun intersectionWith_overlap() {\n        assertEquals(Rect(2, 2, 15, 15), Rect(0, 0, 15, 15).intersectionWith(Rect(2, 2, 17, 17)))\n    }\n\n    @Test\n    @SmallTest\n    fun intersectionWith_noOverlap() {\n        assertFailsWith<IllegalArgumentException>(\n            \"Given rects do not intersect\",\n            fun () {\n                Rect(0, 0, 7, 7).intersectionWith(Rect(7, 7, 15, 15))\n            }\n        )\n    }\n\n    @Test\n    @SmallTest\n    fun move_vertical() {\n        assertEquals(Rect(0, 0, 15, 15), Rect(0, 2, 15, 17).move(0, -2))\n    }\n\n    @Test\n    @SmallTest\n    fun move_horizontal() {\n        assertEquals(Rect(0, 0, 15, 15), Rect(2, 0, 17, 15).move(-2, 0))\n    }\n\n    @Test\n    @SmallTest\n    fun move_both() {\n        assertEquals(Rect(2, 2, 15, 15), Rect(4, 0, 17, 13).move(-2, 2))\n    }\n\n    @Test\n    @SmallTest\n    fun projectRegionOfInterest_smaller() {\n        assertEquals(Rect(2, 14, 16, 28), Size(36, 84).projectRegionOfInterest(Size(18, 42), Rect(4, 28, 32, 56)))\n    }\n\n    @Test\n    @SmallTest\n    fun projectRegionOfInterest_exactFit() {\n        assertEquals(Rect(0, 0, 18, 42), Size(36, 84).projectRegionOfInterest(Size(18, 42), Rect(0, 0, 36, 84)))\n    }\n\n    @Test\n    @SmallTest\n    fun projectRegionOfInterest_larger() {\n        assertEquals(Rect(0, -1, 19, 42), Size(36, 84).projectRegionOfInterest(Size(18, 42), Rect(0, -2, 38, 84)))\n    }\n\n    @Test\n    @SmallTest\n    fun projectRegionOfInterest_noSize() {\n        assertFailsWith<IllegalArgumentException>(\n            \"Cannot project from container with non-positive dimensions\",\n            fun () {\n                Size(0, 0).projectRegionOfInterest(Size(18, 42), Rect(0, -2, 38, 84))\n            }\n        )\n    }\n\n    @Test\n    @SmallTest\n    fun projectRegionOfInterest_offCenter() {\n        assertEquals(Rect(6, 14, 16, 20), Size(36, 84).projectRegionOfInterest(Size(18, 42), Rect(12, 28, 32, 40)))\n    }\n\n    @Test\n    @SmallTest\n    fun minAspectRatioSurroundingSize_squareVertical() {\n        assertEquals(Size(900, 1800), minAspectRatioSurroundingSize(Size(900, 900), 0.5F))\n    }\n\n    @Test\n    @SmallTest\n    fun minAspectRatioSurroundingSize_squareHorizontal() {\n        assertEquals(Size(1800, 900), minAspectRatioSurroundingSize(Size(900, 900), 2F))\n    }\n\n    @Test\n    @SmallTest\n    fun minAspectRatioSurroundingSize_rectangleVerticalToVertical() {\n        assertEquals(Size(900, 1800), minAspectRatioSurroundingSize(Size(900, 1100), 0.5F))\n    }\n\n    @Test\n    @SmallTest\n    fun minAspectRatioSurroundingSize_rectangleVerticalToHorizontal() {\n        assertEquals(Size(2200, 1100), minAspectRatioSurroundingSize(Size(900, 1100), 2F))\n    }\n\n    @Test\n    @SmallTest\n    fun minAspectRatioSurroundingSize_rectangleHorizontalToVertical() {\n        assertEquals(Size(1100, 2200), minAspectRatioSurroundingSize(Size(1100, 900), 0.5F))\n    }\n\n    @Test\n    @SmallTest\n    fun minAspectRatioSurroundingSize_rectangleHorizontalToHorizontal() {\n        assertEquals(Size(1800, 900), minAspectRatioSurroundingSize(Size(1100, 900), 2F))\n    }\n\n    @Test\n    @SmallTest\n    fun adjustSizeToAspectRatio_verticalCrop() {\n        assertEquals(Size(900, 1800), adjustSizeToAspectRatio(Size(900, 2200), 0.5F))\n    }\n\n    @Test\n    @SmallTest\n    fun adjustSizeToAspectRatio_verticalExpand() {\n        assertEquals(Size(900, 1800), adjustSizeToAspectRatio(Size(900, 1600), 0.5F))\n    }\n\n    @Test\n    @SmallTest\n    fun adjustSizeToAspectRatio_horizontalCrop() {\n        assertEquals(Size(1800, 900), adjustSizeToAspectRatio(Size(2200, 900), 2F))\n    }\n\n    @Test\n    @SmallTest\n    fun adjustSizeToAspectRatio_horizontalExpand() {\n        assertEquals(Size(1800, 900), adjustSizeToAspectRatio(Size(1600, 900), 2F))\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/androidTest/java/com/getbouncer/scan/framework/util/AppDetailsTest.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport org.junit.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertTrue\n\nclass AppDetailsTest {\n    private val testContext = InstrumentationRegistry.getInstrumentation().context\n\n    @Test\n    fun appDetails_full() {\n        val appDetails = AppDetails.fromContext(testContext)\n\n        assertEquals(\"com.getbouncer.scan.framework.test\", appDetails.appPackageName)\n        assertEquals(\"\", appDetails.applicationId)\n        assertEquals(\"com.getbouncer.scan.framework\", appDetails.libraryPackageName)\n        assertTrue(appDetails.sdkVersion.startsWith(\"2.\"), \"${appDetails.sdkVersion} does not start with \\\"2.\\\"\")\n        assertEquals(-1, appDetails.sdkVersionCode)\n        assertTrue(appDetails.sdkFlavor.isNotEmpty())\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.getbouncer.scan.framework\" >\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n\n</manifest>\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/Analyzer.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport java.io.Closeable\n\n/**\n * The default number of analyzers to run in parallel.\n */\ninternal const val DEFAULT_ANALYZER_PARALLEL_COUNT = 2\n\n/**\n * An analyzer takes some data as an input, and returns an analyzed output. Analyzers should not\n * contain any state. They must define whether they can run on a multithreaded executor, and provide\n * a means of analyzing input data to return some form of result.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ninterface Analyzer<Input, State, Output> {\n    suspend fun analyze(data: Input, state: State): Output\n}\n\n/**\n * A factory to create analyzers.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ninterface AnalyzerFactory<Input, State, Output, AnalyzerType : Analyzer<Input, State, Output>> {\n    suspend fun newInstance(): AnalyzerType?\n}\n\n/**\n * A pool of analyzers.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class AnalyzerPool<DataFrame, State, Output>(\n    val desiredAnalyzerCount: Int,\n    val analyzers: List<Analyzer<DataFrame, State, Output>>\n) {\n    companion object {\n        @Deprecated(\n            message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n            replaceWith = ReplaceWith(\"StripeCardScan\")\n        )\n        suspend fun <DataFrame, State, Output> of(\n            analyzerFactory: AnalyzerFactory<DataFrame, State, Output, out Analyzer<DataFrame, State, Output>>,\n            desiredAnalyzerCount: Int = DEFAULT_ANALYZER_PARALLEL_COUNT,\n        ) = AnalyzerPool(\n            desiredAnalyzerCount = desiredAnalyzerCount,\n            analyzers = (0 until desiredAnalyzerCount).mapNotNull { analyzerFactory.newInstance() }\n        )\n    }\n\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun closeAllAnalyzers() {\n        // This should be using analyzers.forEach, but doing so seems to require API 24. It's unclear why this won't use\n        // the kotlin.collections version of `forEach`, but it's not during compile.\n        for (it in analyzers) { if (it is Closeable) it.close() }\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/Config.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport com.getbouncer.scan.framework.exception.InvalidBouncerApiKeyException\nimport com.getbouncer.scan.framework.time.Duration\nimport com.getbouncer.scan.framework.time.Rate\nimport com.getbouncer.scan.framework.time.seconds\nimport kotlinx.serialization.json.Json\n\nprivate const val REQUIRED_API_KEY_LENGTH = 32\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nobject Config {\n\n    /**\n     * If set to true, turns on debug information.\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var isDebug: Boolean = false\n\n    /**\n     * A log tag used by this library.\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var logTag: String = \"Bouncer\"\n\n    /**\n     * The API key to interface with Bouncer servers\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var apiKey: String? = null\n        set(value) {\n            if (value != null && value.length != REQUIRED_API_KEY_LENGTH) {\n                throw InvalidBouncerApiKeyException\n            }\n            field = value\n        }\n\n    /**\n     * The JSON configuration to use throughout this SDK.\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var json: Json = Json {\n        ignoreUnknownKeys = true\n        isLenient = true\n        encodeDefaults = true\n    }\n\n    /**\n     * Whether or not to track stats\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    val trackStats: Boolean = true\n\n    /**\n     * Whether or not to upload stats\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var uploadStats: Boolean = true\n\n    /**\n     * Whether or not to display the Bouncer logo\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var displayLogo: Boolean = true\n\n    /**\n     * Whether or not to display the result of the scan to the user\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var displayScanResult: Boolean = true\n\n    /**\n     * If set to true, opt-in to beta versions of the ML models.\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var betaModelOptIn: Boolean = false\n\n    /**\n     * The frame rate of a device that is considered slow will be below this rate.\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var slowDeviceFrameRate = Rate(2, 1.seconds)\n\n    /**\n     * Allow downloading ML models.\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var downloadModels = true\n}\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nobject NetworkConfig {\n\n    /**\n     * The base URL where all network requests will be sent\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var baseUrl = \"https://api.getbouncer.com\"\n\n    /**\n     * Whether or not to compress network request bodies.\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var useCompression: Boolean = false\n\n    /**\n     * The total number of times to try making a network request.\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var retryTotalAttempts: Int = 3\n\n    /**\n     * The delay between network request retries.\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var retryDelay: Duration = 5.seconds\n\n    /**\n     * Status codes that should be retried from bouncer servers.\n     */\n    @JvmStatic\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    var retryStatusCodes: Iterable<Int> = 500..599\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/Fetcher.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport android.content.Context\nimport android.util.Log\nimport com.getbouncer.scan.framework.api.NetworkResult\nimport com.getbouncer.scan.framework.api.downloadFileWithRetries\nimport com.getbouncer.scan.framework.api.getModelDetails\nimport com.getbouncer.scan.framework.api.getModelSignedUrl\nimport com.getbouncer.scan.framework.time.ClockMark\nimport com.getbouncer.scan.framework.time.asEpochMillisecondsClockMark\nimport com.getbouncer.scan.framework.time.days\nimport com.getbouncer.scan.framework.util.HashMismatchException\nimport com.getbouncer.scan.framework.util.calculateHash\nimport com.getbouncer.scan.framework.util.fileMatchesHash\nimport com.getbouncer.scan.framework.util.memoizeSuspend\nimport com.getbouncer.scan.framework.util.sanitizeFileName\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.withContext\nimport java.io.File\nimport java.io.IOException\nimport java.net.URL\nimport java.security.NoSuchAlgorithmException\n\nprivate const val CACHE_MODEL_MAX_COUNT = 3\n\nprivate const val PURPOSE_MODEL_UPGRADE = \"model_upgrade\"\n\n/**\n * Fetched data metadata.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nsealed class FetchedModelMeta(open val modelVersion: String, open val hashAlgorithm: String)\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class FetchedModelFileMeta(\n    override val modelVersion: String,\n    override val hashAlgorithm: String,\n    val modelFile: File?,\n) : FetchedModelMeta(modelVersion, hashAlgorithm)\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class FetchedModelResourceMeta(\n    override val modelVersion: String,\n    override val hashAlgorithm: String,\n    val hash: String,\n    val assetFileName: String?,\n) : FetchedModelMeta(modelVersion, hashAlgorithm)\n\n/**\n * Fetched data information.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nsealed class FetchedData(\n    open val modelClass: String,\n    open val modelFrameworkVersion: Int,\n    open val modelVersion: String,\n    open val modelHash: String?,\n    open val modelHashAlgorithm: String?,\n) {\n    companion object {\n        @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n        fun fromFetchedModelMeta(modelClass: String, modelFrameworkVersion: Int, meta: FetchedModelMeta) = when (meta) {\n            is FetchedModelFileMeta ->\n                FetchedFile(\n                    modelClass = modelClass,\n                    modelFrameworkVersion = modelFrameworkVersion,\n                    modelVersion = meta.modelVersion,\n                    modelHash = meta.modelFile?.let { runBlocking { try { calculateHash(it, meta.hashAlgorithm) } catch (t: Throwable) { null } } },\n                    modelHashAlgorithm = meta.hashAlgorithm,\n                    file = meta.modelFile\n                )\n            is FetchedModelResourceMeta ->\n                FetchedResource(\n                    modelClass = modelClass,\n                    modelFrameworkVersion = modelFrameworkVersion,\n                    modelVersion = meta.modelVersion,\n                    modelHash = meta.hash,\n                    modelHashAlgorithm = meta.hashAlgorithm,\n                    assetFileName = meta.assetFileName,\n                )\n        }\n    }\n\n    abstract val successfullyFetched: Boolean\n}\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class FetchedResource(\n    override val modelClass: String,\n    override val modelFrameworkVersion: Int,\n    override val modelVersion: String,\n    override val modelHash: String?,\n    override val modelHashAlgorithm: String?,\n    val assetFileName: String?,\n) : FetchedData(modelClass, modelFrameworkVersion, modelVersion, modelHash, modelHashAlgorithm) {\n    override val successfullyFetched: Boolean = assetFileName != null\n}\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class FetchedFile(\n    override val modelClass: String,\n    override val modelFrameworkVersion: Int,\n    override val modelVersion: String,\n    override val modelHash: String?,\n    override val modelHashAlgorithm: String?,\n    val file: File?,\n) : FetchedData(modelClass, modelFrameworkVersion, modelVersion, modelHash, modelHashAlgorithm) {\n    override val successfullyFetched: Boolean = modelHash != null\n}\n\n/**\n * An interface for getting data ready to be loaded into memory.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ninterface Fetcher {\n    val modelClass: String\n    val modelFrameworkVersion: Int\n\n    /**\n     * Prepare data to be loaded into memory. If the fetched data is to be used immediately, the fetcher will prioritize\n     * fetching from the cache over getting the latest version.\n     *\n     * @param forImmediateUse: if there is a cached version of the model, return that immediately instead of downloading a new model\n     */\n    suspend fun fetchData(forImmediateUse: Boolean, isOptional: Boolean): FetchedData\n\n    suspend fun isCached(): Boolean\n}\n\n/**\n * A [Fetcher] that gets data from android resources.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nabstract class ResourceFetcher : Fetcher {\n    protected abstract val modelVersion: String\n    protected abstract val hash: String\n    protected abstract val hashAlgorithm: String\n    protected abstract val assetFileName: String\n\n    override suspend fun fetchData(forImmediateUse: Boolean, isOptional: Boolean): FetchedResource =\n        FetchedResource(\n            modelClass = modelClass,\n            modelFrameworkVersion = modelFrameworkVersion,\n            modelVersion = modelVersion,\n            modelHash = hash,\n            modelHashAlgorithm = hashAlgorithm,\n            assetFileName = assetFileName,\n        )\n\n    override suspend fun isCached(): Boolean = true\n}\n\n/**\n * A [Fetcher] that downloads data from the web.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nsealed class WebFetcher(protected val context: Context) : Fetcher {\n    protected data class DownloadDetails(val url: URL, val hash: String, val hashAlgorithm: String, val modelVersion: String)\n\n    /**\n     * Keep track of any exceptions that occurred when fetching data  after the specified number of retries. This is\n     * used to prevent the fetcher from repeatedly trying to fetch the data from multiple threads after the number of\n     * retries has been reached.\n     */\n    private var fetchException: Throwable? = null\n\n    override suspend fun fetchData(forImmediateUse: Boolean, isOptional: Boolean): FetchedData {\n        val stat = Stats.trackPersistentRepeatingTask(\"web_fetcher_$modelClass\")\n        val cachedData = FetchedData.fromFetchedModelMeta(modelClass, modelFrameworkVersion, tryFetchLatestCachedData())\n\n        // attempt to fetch the data from local cache if it's needed immediately or downloading is not allowed\n        if (forImmediateUse || !Config.downloadModels) {\n            tryFetchLatestCachedData().run {\n                val data = FetchedData.fromFetchedModelMeta(modelClass, modelFrameworkVersion, this)\n                if (data.successfullyFetched) {\n                    Log.d(Config.logTag, \"Fetcher: $modelClass is needed immediately and cached version ${data.modelVersion} is available.\")\n                    stat.trackResult(\"success\")\n                    return@fetchData data\n                }\n            }\n        }\n\n        // if downloading models is not allowed, return an empty fetched data\n        if (!Config.downloadModels) {\n            Log.d(Config.logTag, \"Fetcher: $modelClass cannot be downloaded since downloads are turned off\")\n            stat.trackResult(\"downloads_disabled\")\n            return FetchedData.fromFetchedModelMeta(\n                modelClass = modelClass,\n                modelFrameworkVersion = modelFrameworkVersion,\n                meta = FetchedModelFileMeta(\n                    modelVersion = cachedData.modelVersion,\n                    hashAlgorithm = cachedData.modelHashAlgorithm ?: \"\",\n                    modelFile = null,\n                ),\n            )\n        }\n\n        // get details for downloading the data. If download details cannot be retrieved, use the latest cached version\n        val downloadDetails = fetchDownloadDetails(cachedData.modelHash, cachedData.modelHashAlgorithm) ?: run {\n            Log.d(Config.logTag, \"Fetcher: not downloading $modelClass, using cached version ${cachedData.modelVersion}\")\n            stat.trackResult(\"no_download_details\")\n            return@fetchData cachedData\n        }\n\n        // if no cache is available, this is needed immediately, and this is optional, return a download failure\n        if (forImmediateUse && isOptional) {\n            Log.d(Config.logTag, \"Fetcher: optional $modelClass needed for immediate use, but no cache available.\")\n            stat.trackResult(\"optional_model_not_downloaded\")\n            return FetchedData.fromFetchedModelMeta(\n                modelClass = modelClass,\n                modelFrameworkVersion = modelFrameworkVersion,\n                meta = FetchedModelFileMeta(\n                    modelVersion = downloadDetails.modelVersion,\n                    hashAlgorithm = downloadDetails.hashAlgorithm,\n                    modelFile = null,\n                ),\n            )\n        }\n\n        return try {\n            // check the local cache for a matching model\n            tryFetchMatchingCachedFile(downloadDetails.hash, downloadDetails.hashAlgorithm).run {\n                val data = FetchedData.fromFetchedModelMeta(modelClass, modelFrameworkVersion, this)\n                if (data.successfullyFetched) {\n                    Log.d(Config.logTag, \"Fetcher: $modelClass already has latest version downloaded.\")\n                    stat.trackResult(\"success_cached\")\n                    return@fetchData data\n                }\n            }\n\n            downloadData(downloadDetails).also {\n                if (it.successfullyFetched) {\n                    Log.d(Config.logTag, \"Fetcher: $modelClass successfully downloaded.\")\n                    stat.trackResult(\"success_downloaded\")\n                } else {\n                    Log.d(Config.logTag, \"Fetcher: $modelClass failed to download from $downloadDetails.\")\n                    stat.trackResult(\"download_failed\")\n                }\n            }\n        } catch (t: Throwable) {\n            fetchException = t\n            if (cachedData.successfullyFetched) {\n                Log.w(Config.logTag, \"Fetcher: Failed to download model $modelClass, loaded from local cache\", t)\n                stat.trackResult(\"success_download_failed_but_cached\")\n            } else {\n                Log.e(Config.logTag, \"Fetcher: Failed to download model $modelClass, no local cache available\", t)\n                stat.trackResult(t::class.java.simpleName)\n            }\n            cachedData\n        }\n    }\n\n    override suspend fun isCached(): Boolean = when (val meta = tryFetchLatestCachedData()) {\n        is FetchedModelFileMeta -> meta.modelFile != null\n        is FetchedModelResourceMeta -> true\n    }\n\n    /**\n     * Get information about what version of the model to download.\n     */\n    private val fetchDownloadDetails = memoizeSuspend(3.days) { cachedHash: String?, cachedHashAlgorithm: String? ->\n        getDownloadDetails(cachedHash, cachedHashAlgorithm)\n    }\n\n    /**\n     * Download the data using memoization so that data is only downloaded once.\n     */\n    private val downloadData = memoizeSuspend { downloadDetails: DownloadDetails ->\n        val downloadOutputFile = getDownloadOutputFile(downloadDetails.modelVersion)\n\n        // if a previous exception was encountered, attempt to fetch cached data\n        fetchException?.run {\n            Log.d(Config.logTag, \"Fetcher: Previous exception encountered for $modelClass, rethrowing\")\n            throw this\n        }\n\n        try {\n            downloadAndVerify(\n                context = context,\n                url = downloadDetails.url,\n                outputFile = downloadOutputFile,\n                hash = downloadDetails.hash,\n                hashAlgorithm = downloadDetails.hashAlgorithm,\n            )\n\n            Log.d(Config.logTag, \"Fetcher: $modelClass downloaded version ${downloadDetails.modelVersion}\")\n            return@memoizeSuspend FetchedFile(\n                modelClass = modelClass,\n                modelFrameworkVersion = modelFrameworkVersion,\n                modelVersion = downloadDetails.modelVersion,\n                modelHash = downloadDetails.hash,\n                modelHashAlgorithm = downloadDetails.hashAlgorithm,\n                file = downloadOutputFile,\n            )\n        } finally {\n            cleanUpPostDownload(downloadOutputFile)\n        }\n    }\n\n    /**\n     * Attempt to load the data from the local cache.\n     */\n    protected abstract suspend fun tryFetchLatestCachedData(): FetchedModelMeta\n\n    /**\n     * Attempt to load a cached data given the required [hash] and [hashAlgorithm].\n     */\n    protected abstract suspend fun tryFetchMatchingCachedFile(hash: String, hashAlgorithm: String): FetchedModelMeta\n\n    /**\n     * Get [DownloadDetails] for the data that will be downloaded.\n     *\n     * @param cachedModelHash: the hash of the cached model, or null if nothing is cached\n     * @param cachedModelHashAlgorithm: the hash algorithm used to calculate the hash\n     */\n    protected abstract suspend fun getDownloadDetails(\n        cachedModelHash: String?,\n        cachedModelHashAlgorithm: String?,\n    ): DownloadDetails?\n\n    /**\n     * Get the file where the data should be downloaded.\n     */\n    protected abstract suspend fun getDownloadOutputFile(modelVersion: String): File\n\n    /**\n     * After download, clean up.\n     */\n    protected abstract suspend fun cleanUpPostDownload(downloadedFile: File)\n\n    /**\n     * Clear the cache for this loader. This will force new downloads.\n     */\n    abstract suspend fun clearCache()\n}\n\n/**\n * A [WebFetcher] that directly downloads a model.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nabstract class DirectDownloadWebFetcher(context: Context) : WebFetcher(context) {\n    abstract val url: URL\n    abstract val hash: String\n    abstract val hashAlgorithm: String\n    abstract val modelVersion: String\n\n    private val localFileName: String by lazy { url.path.replace('/', '_') }\n\n    override suspend fun tryFetchLatestCachedData(): FetchedModelMeta {\n        val localFile = getDownloadOutputFile(modelVersion)\n        return if (fileMatchesHash(localFile, hash, hashAlgorithm)) {\n            FetchedModelFileMeta(modelVersion, hashAlgorithm, localFile)\n        } else {\n            FetchedModelFileMeta(modelVersion, hashAlgorithm, null)\n        }\n    }\n\n    override suspend fun tryFetchMatchingCachedFile(hash: String, hashAlgorithm: String): FetchedModelMeta =\n        FetchedModelFileMeta(modelVersion, hashAlgorithm, null)\n\n    override suspend fun getDownloadOutputFile(modelVersion: String) =\n        File(context.cacheDir, sanitizeFileName(localFileName))\n\n    override suspend fun getDownloadDetails(\n        cachedModelHash: String?,\n        cachedModelHashAlgorithm: String?,\n    ): DownloadDetails? =\n        DownloadDetails(url, hash, hashAlgorithm, modelVersion)\n\n    override suspend fun cleanUpPostDownload(downloadedFile: File) { /* nothing to do */ }\n\n    override suspend fun clearCache() {\n        val localFile = getDownloadOutputFile(modelVersion)\n        if (localFile.exists()) {\n            localFile.delete()\n        }\n    }\n}\n\n/**\n * A [WebFetcher] that uses the signed URL server endpoints to download data.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nabstract class SignedUrlModelWebFetcher(context: Context) : DirectDownloadWebFetcher(context) {\n    abstract val modelFileName: String\n\n    private val localFileName by lazy { \"${modelClass}_${modelFileName}_$modelVersion\" }\n\n    // this field is not used by this class\n    override val url: URL = URL(NetworkConfig.baseUrl)\n\n    override suspend fun getDownloadOutputFile(modelVersion: String) = File(context.cacheDir, sanitizeFileName(localFileName))\n\n    override suspend fun getDownloadDetails(\n        cachedModelHash: String?,\n        cachedModelHashAlgorithm: String?,\n    ) = when (val signedUrlResponse = getModelSignedUrl(context, modelClass, modelVersion, modelFileName)) {\n        is NetworkResult.Success ->\n            try {\n                URL(signedUrlResponse.body.modelUrl)\n            } catch (t: Throwable) {\n                Log.e(Config.logTag, \"Fetcher: Invalid signed url for model $modelClass: ${signedUrlResponse.body.modelUrl}\", t)\n                null\n            }\n        is NetworkResult.Error -> {\n            Log.w(Config.logTag, \"Fetcher: Failed to get signed url for model $modelClass: ${signedUrlResponse.error}\")\n            null\n        }\n        is NetworkResult.Exception -> {\n            Log.e(Config.logTag, \"Fetcher: Exception fetching signed url for model $modelClass: ${signedUrlResponse.responseCode}\", signedUrlResponse.exception)\n            null\n        }\n    }?.let { DownloadDetails(it, hash, hashAlgorithm, modelVersion) }\n}\n\n/**\n * A [WebFetcher] that queries Bouncer servers for updated data. If a new version is found, download it. If the data\n * details match what is cached, return the cached version instead.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nabstract class UpdatingModelWebFetcher(context: Context) : SignedUrlModelWebFetcher(context) {\n    abstract val defaultModelVersion: String\n    abstract val defaultModelFileName: String\n    abstract val defaultModelHash: String\n    abstract val defaultModelHashAlgorithm: String\n\n    private var cachedDownloadDetails: DownloadDetails? = null\n\n    private val getCacheFolder = memoizeSuspend<File> {\n        ensureLocalFolder(\"${modelClass}_$modelFrameworkVersion\")\n    }\n\n    override val modelVersion: String by lazy { defaultModelVersion }\n    override val modelFileName: String by lazy { defaultModelFileName }\n    override val hash: String by lazy { defaultModelHash }\n    override val hashAlgorithm: String by lazy { defaultModelHashAlgorithm }\n\n    override suspend fun tryFetchLatestCachedData(): FetchedModelMeta =\n        getLatestFile()?.let { FetchedModelFileMeta(it.name, defaultModelHashAlgorithm, it) } ?: FetchedModelFileMeta(defaultModelVersion, defaultModelHashAlgorithm, null)\n\n    override suspend fun tryFetchMatchingCachedFile(hash: String, hashAlgorithm: String): FetchedModelMeta =\n        getMatchingFile(hash, hashAlgorithm)?.let { FetchedModelFileMeta(it.name, defaultModelHashAlgorithm, it) } ?: FetchedModelFileMeta(defaultModelVersion, defaultModelHashAlgorithm, null)\n\n    override suspend fun getDownloadOutputFile(modelVersion: String) =\n        File(getCacheFolder(), sanitizeFileName(modelVersion))\n\n    override suspend fun getDownloadDetails(\n        cachedModelHash: String?,\n        cachedModelHashAlgorithm: String?,\n    ): DownloadDetails? {\n        cachedDownloadDetails?.let { return DownloadDetails(url, hash, hashAlgorithm, modelVersion) }\n\n        val nextUpgradeTime = getNextUpgradeTime()\n        when {\n            Config.betaModelOptIn ->\n                Log.d(Config.logTag, \"Fetcher: Beta opt-in, attempting to upgrade $modelClass\")\n            nextUpgradeTime.hasPassed() ->\n                Log.d(Config.logTag, \"Fetcher: Time to upgrade $modelClass, fetching upgrade details\")\n            cachedModelHash == null ->\n                Log.d(Config.logTag, \"Fetcher: Downloading initial version of $modelClass\")\n            else -> {\n                Log.d(Config.logTag, \"Fetcher: Not yet time to upgrade $modelClass (will upgrade at $nextUpgradeTime)\")\n                return null\n            }\n        }\n\n        return when (\n            val detailsResponse = getModelDetails(\n                context = context,\n                modelClass = modelClass,\n                modelFrameworkVersion = modelFrameworkVersion,\n                cachedModelHash = cachedModelHash,\n                cachedModelHashAlgorithm = cachedModelHashAlgorithm,\n            )\n        ) {\n            is NetworkResult.Success ->\n                try {\n                    detailsResponse.body.queryAgainAfterMs?.asEpochMillisecondsClockMark()?.apply {\n                        setNextModelUpgradeAttemptTime(this)\n                    }\n                    detailsResponse.body.url?.let {\n                        DownloadDetails(\n                            url = URL(it),\n                            hash = detailsResponse.body.hash,\n                            hashAlgorithm = detailsResponse.body.hashAlgorithm,\n                            modelVersion = detailsResponse.body.modelVersion,\n                        ).apply { cachedDownloadDetails = this }\n                    }\n                } catch (t: Throwable) {\n                    Log.e(Config.logTag, \"Fetcher: Invalid signed url for model $modelClass: ${detailsResponse.body.url}\", t)\n                    null\n                }\n            is NetworkResult.Error -> {\n                Log.w(Config.logTag, \"Fetcher: Failed to get latest details for model $modelClass: ${detailsResponse.error}\")\n                fallbackDownloadDetails()\n            }\n            is NetworkResult.Exception -> {\n                Log.e(Config.logTag, \"Fetcher: Exception retrieving latest details for model $modelClass: ${detailsResponse.responseCode}\", detailsResponse.exception)\n                fallbackDownloadDetails()\n            }\n        }\n    }\n\n    /**\n     * Determine if we should query for a model upgrade\n     */\n    protected open fun getNextUpgradeTime(): ClockMark =\n        StorageFactory\n            .getStorageInstance(context, PURPOSE_MODEL_UPGRADE)\n            .getLong(modelClass, 0)\n            .asEpochMillisecondsClockMark()\n\n    protected open fun setNextModelUpgradeAttemptTime(time: ClockMark) {\n        StorageFactory\n            .getStorageInstance(context, PURPOSE_MODEL_UPGRADE)\n            .storeValue(modelClass, time.toMillisecondsSinceEpoch())\n    }\n\n    protected open fun clearNextUpgradeTime() {\n        StorageFactory\n            .getStorageInstance(context, PURPOSE_MODEL_UPGRADE)\n            .remove(modelClass)\n    }\n\n    /**\n     * Fall back to getting the download details.\n     */\n    protected open suspend fun fallbackDownloadDetails() =\n        super.getDownloadDetails(null, null)?.apply { cachedDownloadDetails = this }\n\n    /**\n     * Delete all files in cache that are not the recently downloaded file.\n     */\n    override suspend fun cleanUpPostDownload(downloadedFile: File) = withContext(Dispatchers.IO) {\n        try {\n            getCacheFolder()\n                .listFiles()\n                ?.filter { it != downloadedFile && calculateHash(it, defaultModelHashAlgorithm) != defaultModelHash }\n                ?.sortedByDescending { it.lastModified() }\n                ?.filterIndexed { index, _ -> index > CACHE_MODEL_MAX_COUNT }\n                ?.forEach { it.delete() }\n        } catch (t: Throwable) {\n            Log.e(Config.logTag, \"Error cleaning up post download\", t)\n        }.let { }\n    }\n\n    /**\n     * If a file in the cache directory matches the provided [hash], return it.\n     */\n    private suspend fun getMatchingFile(hash: String, hashAlgorithm: String): File? =\n        withContext(Dispatchers.IO) {\n            try {\n                getCacheFolder()\n                    .listFiles()\n                    ?.sortedByDescending { it.lastModified() }\n                    ?.firstOrNull { calculateHash(it, hashAlgorithm) == hash }\n            } catch (t: Throwable) {\n                Log.e(Config.logTag, \"Unable to get matching file\", t)\n                null\n            }\n        }\n\n    /**\n     * Get the highest model version, or most recently created file in the cache folder. Return null\n     * if no files in cache\n     */\n    private suspend fun getLatestFile(): File? = withContext(Dispatchers.IO) {\n        val files = getCacheFolder()\n            .listFiles()\n            ?.filter { it.exists() && it.length() > 0 }\n\n        files\n            ?.filter { it.name.startsWith(\"1.\") }\n            ?.mapNotNull { file -> ModelVersion.fromString(file.name)?.let { it to file } }\n            ?.maxByOrNull { it.first }\n            ?.second ?: files?.maxByOrNull { it.lastModified() }\n    }\n\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    data class ModelVersion(\n        val versioningVersion: Int,\n        val frameworkVersion: Int,\n        val modelNumber: Int,\n        val quantization: Int,\n    ) : Comparable<ModelVersion> {\n        override fun compareTo(other: ModelVersion): Int {\n            val versioningDiff = versioningVersion.compareTo(other.versioningVersion)\n            val frameworkDiff = frameworkVersion.compareTo(other.frameworkVersion)\n            val modelDiff = modelNumber.compareTo(other.modelNumber)\n\n            return when {\n                versioningDiff != 0 -> versioningDiff\n                frameworkDiff != 0 -> frameworkDiff\n                modelDiff != 0 -> modelDiff\n                else -> 0\n            }\n        }\n\n        companion object {\n            fun fromString(modelVersion: String): ModelVersion? {\n                val components = modelVersion.split(\"\\\\.\").mapNotNull {\n                    try { it.toInt() } catch (t: Throwable) { null }\n                }\n\n                if (components.size != 4) {\n                    return null\n                }\n\n                return ModelVersion(\n                    components[0],\n                    components[1],\n                    components[2],\n                    components[3],\n                )\n            }\n        }\n    }\n\n    /**\n     * Ensure that the local folder exists and get it.\n     */\n    private suspend fun ensureLocalFolder(folderName: String): File = withContext(Dispatchers.IO) {\n        val localFolder = File(context.cacheDir, folderName)\n        if (localFolder.exists() && !localFolder.isDirectory) {\n            localFolder.delete()\n        }\n        if (!localFolder.exists()) {\n            localFolder.mkdirs()\n        }\n        localFolder\n    }\n\n    /**\n     * Force re-download of models by clearing the cache.\n     */\n    override suspend fun clearCache() = withContext(Dispatchers.IO) {\n        getCacheFolder().deleteRecursively()\n        getCacheFolder().mkdirs()\n\n        clearNextUpgradeTime()\n    }.let { }\n}\n\n/**\n * A [WebFetcher] that queries Bouncer servers for updated data. If a new version is found, download it. If the data\n * details match what is cached, return the cached version instead.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nabstract class UpdatingResourceFetcher(context: Context) : UpdatingModelWebFetcher(context) {\n    protected abstract val assetFileName: String\n    protected abstract val resourceModelVersion: String\n    protected abstract val resourceModelHash: String\n    protected abstract val resourceModelHashAlgorithm: String\n\n    override val defaultModelFileName: String = \"\"\n    override val defaultModelVersion: String by lazy { resourceModelVersion }\n    override val defaultModelHash: String by lazy { resourceModelHash }\n    override val defaultModelHashAlgorithm: String by lazy { resourceModelHashAlgorithm }\n\n    override suspend fun tryFetchLatestCachedData() = super.tryFetchLatestCachedData().run {\n        when (this) {\n            is FetchedModelFileMeta -> if (modelFile == null) fetchModelFromResource() else this\n            is FetchedModelResourceMeta -> this\n        }\n    }\n\n    override suspend fun tryFetchMatchingCachedFile(hash: String, hashAlgorithm: String): FetchedModelMeta =\n        if (hash == defaultModelHash && hashAlgorithm == defaultModelHashAlgorithm) {\n            fetchModelFromResource()\n        } else {\n            super.tryFetchMatchingCachedFile(hash, hashAlgorithm)\n        }\n\n    override suspend fun fallbackDownloadDetails(): DownloadDetails? = DownloadDetails(\n        url = URL(\"https://localhost\"),\n        hash = resourceModelHash,\n        hashAlgorithm = resourceModelHashAlgorithm,\n        modelVersion = resourceModelVersion,\n    )\n\n    private fun fetchModelFromResource(): FetchedModelMeta =\n        FetchedModelResourceMeta(\n            modelVersion = resourceModelVersion,\n            assetFileName = assetFileName,\n            hash = resourceModelHash,\n            hashAlgorithm = resourceModelHashAlgorithm,\n        )\n}\n\n/**\n * Download a file from a given [url] and ensure that it matches the expected [hash].\n */\n@Throws(IOException::class, NoSuchAlgorithmException::class, HashMismatchException::class)\nprivate suspend fun downloadAndVerify(\n    context: Context,\n    url: URL,\n    outputFile: File,\n    hash: String,\n    hashAlgorithm: String\n) {\n    downloadFile(context, url, outputFile)\n    val calculatedHash = calculateHash(outputFile, hashAlgorithm)\n\n    if (hash != calculatedHash) {\n        withContext(Dispatchers.IO) { outputFile.delete() }\n        throw HashMismatchException(hashAlgorithm, hash, calculatedHash)\n    }\n}\n\n/**\n * Download a file from the provided [url] into the provided [outputFile].\n */\n@Throws(IOException::class, FileAlreadyExistsException::class, NoSuchFileException::class)\nprivate suspend fun downloadFile(context: Context, url: URL, outputFile: File) = withContext(Dispatchers.IO) {\n    if (outputFile.exists()) {\n        outputFile.delete()\n    }\n    downloadFileWithRetries(context, url, outputFile)\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/Loader.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport android.content.Context\nimport android.util.Log\nimport com.getbouncer.scan.framework.ml.trackModelLoaded\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport java.io.File\nimport java.io.FileInputStream\nimport java.io.IOException\nimport java.nio.ByteBuffer\nimport java.nio.channels.FileChannel\n\n/**\n * An interface for loading data into a byte buffer.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass Loader(private val context: Context) {\n\n    /**\n     * Load previously fetched data into memory.\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    suspend fun loadData(fetchedData: FetchedData): ByteBuffer? = when (fetchedData) {\n        is FetchedResource -> loadResourceData(fetchedData)\n        is FetchedFile -> loadFileData(fetchedData)\n    }\n\n    /**\n     * Create a [ByteBuffer] object from an android resource.\n     */\n    private suspend fun loadResourceData(fetchedData: FetchedResource): ByteBuffer? {\n        val stat = Stats.trackPersistentRepeatingTask(\"resource_loader:${fetchedData.modelClass}\")\n\n        if (fetchedData.assetFileName == null) {\n            trackModelLoaded(fetchedData.modelClass, fetchedData.modelVersion, fetchedData.modelFrameworkVersion, false)\n            stat.trackResult(\"failure:${fetchedData.modelClass}\")\n            return null\n        }\n\n        return try {\n            val loadedData = readAssetToByteBuffer(context, fetchedData.assetFileName)\n            stat.trackResult(\"success\")\n            trackModelLoaded(fetchedData.modelClass, fetchedData.modelVersion, fetchedData.modelFrameworkVersion, true)\n            loadedData\n        } catch (t: Throwable) {\n            Log.e(Config.logTag, \"Failed to load resource\", t)\n            trackModelLoaded(fetchedData.modelClass, fetchedData.modelVersion, fetchedData.modelFrameworkVersion, false)\n            null\n        }\n    }\n\n    /**\n     * Create a [ByteBuffer] object from a [File].\n     */\n    private suspend fun loadFileData(fetchedData: FetchedFile): ByteBuffer? {\n        val stat = Stats.trackPersistentRepeatingTask(\"web_loader:${fetchedData.modelClass}\")\n\n        if (fetchedData.file == null) {\n            trackModelLoaded(fetchedData.modelClass, fetchedData.modelVersion, fetchedData.modelFrameworkVersion, false)\n            stat.trackResult(\"failure:${fetchedData.modelClass}\")\n            return null\n        }\n\n        return try {\n            val loadedData = readFileToByteBuffer(fetchedData.file)\n            stat.trackResult(\"success\")\n            trackModelLoaded(fetchedData.modelClass, fetchedData.modelVersion, fetchedData.modelFrameworkVersion, true)\n            loadedData\n        } catch (t: Throwable) {\n            stat.trackResult(\"failure:${fetchedData.modelClass}\")\n            trackModelLoaded(fetchedData.modelClass, fetchedData.modelVersion, fetchedData.modelFrameworkVersion, true)\n            null\n        }\n    }\n}\n\n/**\n * Read a [File] into a [ByteBuffer].\n */\nprivate suspend fun readFileToByteBuffer(file: File) = withContext(Dispatchers.IO) {\n    FileInputStream(file).use { readFileToByteBuffer(it, 0, file.length()) }\n}\n\n/**\n * Read a raw resource into a [ByteBuffer].\n */\nprivate suspend fun readAssetToByteBuffer(context: Context, assetFileName: String) =\n    withContext(Dispatchers.IO) {\n        context.assets.openFd(assetFileName).use { fileDescriptor ->\n            FileInputStream(fileDescriptor.fileDescriptor).use { input ->\n                readFileToByteBuffer(\n                    input,\n                    fileDescriptor.startOffset,\n                    fileDescriptor.declaredLength,\n                )\n            }\n        }\n    }\n\n/**\n * Read a [fileInputStream] into a [ByteBuffer].\n */\n@Throws(IOException::class)\nprivate fun readFileToByteBuffer(\n    fileInputStream: FileInputStream,\n    startOffset: Long,\n    declaredLength: Long\n): ByteBuffer = fileInputStream.channel.map(\n    FileChannel.MapMode.READ_ONLY,\n    startOffset,\n    declaredLength\n)\n\n/**\n * Determine if an asset file exists\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nfun assetFileExists(context: Context, assetFileName: String) =\n    try {\n        context.assets.openFd(assetFileName).use { it.declaredLength > 0 }\n    } catch (t: Throwable) {\n        false\n    }\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/Loop.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport com.getbouncer.scan.framework.time.Clock\nimport com.getbouncer.scan.framework.time.ClockMark\nimport com.getbouncer.scan.framework.time.Duration\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.buffer\nimport kotlinx.coroutines.flow.channelFlow\nimport kotlinx.coroutines.flow.collect\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.withContext\nimport java.util.concurrent.atomic.AtomicBoolean\nimport java.util.concurrent.atomic.AtomicInteger\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nobject NoAnalyzersAvailableException : Exception()\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nobject AlreadySubscribedException : Exception()\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ninterface AnalyzerLoopErrorListener {\n\n    /**\n     * A failure occurred during frame analysis. If this returns true, the loop will terminate. If this returns false,\n     * the loop will continue to execute on new data.\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun onAnalyzerFailure(t: Throwable): Boolean\n\n    /**\n     * A failure occurred while collecting the result of frame analysis. If this returns true, the loop will terminate.\n     * If this returns false, the loop will continue to execute on new data.\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun onResultFailure(t: Throwable): Boolean\n}\n\n/**\n * A loop to execute repeated analysis. The loop uses coroutines to run the [Analyzer.analyze] method. If the [Analyzer]\n * is threadsafe, multiple coroutines will be used. If not, a single coroutine will be used.\n *\n * Any data enqueued while the analyzers are at capacity will be dropped.\n *\n * This will process data until the result aggregator returns true.\n *\n * Note: an analyzer loop can only be started once. Once it terminates, it cannot be restarted.\n *\n * @param analyzerPool: A pool of analyzers to use in this loop.\n * @param analyzerLoopErrorListener: An error handler for this loop\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nsealed class AnalyzerLoop<DataFrame, State, Output>(\n    private val analyzerPool: AnalyzerPool<DataFrame, in State, Output>,\n    private val analyzerLoopErrorListener: AnalyzerLoopErrorListener,\n) : ResultHandler<DataFrame, Output, Boolean> {\n    private val started = AtomicBoolean(false)\n    protected var startedAt: ClockMark? = null\n    private var finished: Boolean = false\n\n    private val cancelMutex = Mutex()\n\n    private lateinit var loopExecutionStatTracker: StatTracker\n\n    private var workerJob: Job? = null\n\n    protected fun subscribeToFlow(flow: Flow<DataFrame>, processingCoroutineScope: CoroutineScope): Job? {\n        if (!started.getAndSet(true)) {\n            startedAt = Clock.markNow()\n        } else {\n            analyzerLoopErrorListener.onAnalyzerFailure(AlreadySubscribedException)\n            return null\n        }\n\n        loopExecutionStatTracker = Stats.trackTask(\"${this::class.java.simpleName}_execution\")\n\n        if (analyzerPool.analyzers.isEmpty()) {\n            processingCoroutineScope.launch { loopExecutionStatTracker.trackResult(\"canceled\") }\n            analyzerLoopErrorListener.onAnalyzerFailure(NoAnalyzersAvailableException)\n            return null\n        }\n\n        workerJob = processingCoroutineScope.launch {\n            // This should be using analyzerPool.analyzers.forEach, but doing so seems to require API 24. It's unclear\n            // why this won't use the kotlin.collections version of `forEach`, but it's not during compile.\n            for (it in analyzerPool.analyzers) {\n                launch(Dispatchers.Default) {\n                    startWorker(flow, it)\n                }\n            }\n        }\n\n        return workerJob\n    }\n\n    protected suspend fun unsubscribeFromFlow() = cancelMutex.withLock {\n        workerJob?.apply { if (isActive) { cancel() } }\n        workerJob = null\n        started.set(false)\n        finished = false\n    }\n\n    /**\n     * Launch a worker coroutine that has access to the analyzer's `analyze` method and the result handler\n     */\n    private suspend fun startWorker(\n        flow: Flow<DataFrame>,\n        analyzer: Analyzer<DataFrame, in State, Output>,\n    ) {\n        flow.collect { frame ->\n            val stat = Stats.trackRepeatingTask(\"analyzer_execution:${analyzer::class.java.simpleName}\")\n            try {\n                val analyzerResult = analyzer.analyze(frame, getState())\n\n                try {\n                    finished = onResult(analyzerResult, frame)\n                } catch (t: Throwable) {\n                    stat.trackResult(\"result_failure\")\n                    handleResultFailure(t)\n                }\n            } catch (t: Throwable) {\n                stat.trackResult(\"analyzer_failure\")\n                handleAnalyzerFailure(t)\n            }\n\n            if (finished) {\n                loopExecutionStatTracker.trackResult(\"success\")\n                unsubscribeFromFlow()\n            }\n\n            stat.trackResult(\"success\")\n        }\n    }\n\n    private suspend fun handleAnalyzerFailure(t: Throwable) {\n        if (withContext(Dispatchers.Main) { analyzerLoopErrorListener.onAnalyzerFailure(t) }) { unsubscribeFromFlow() }\n    }\n\n    private suspend fun handleResultFailure(t: Throwable) {\n        if (withContext(Dispatchers.Main) { analyzerLoopErrorListener.onResultFailure(t) }) { unsubscribeFromFlow() }\n    }\n\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    abstract fun getState(): State\n}\n\n/**\n * This kind of [AnalyzerLoop] will process data until the result handler indicates that it has reached a terminal\n * state and is no longer listening.\n *\n * Data can be added to a queue for processing by a camera or other producer. It will be consumed by FILO. If no data\n * is available, the analyzer pauses until data becomes available.\n *\n * If the enqueued data exceeds the allowed memory size, the bottom of the data stack will be dropped and will not be\n * processed. This alleviates memory pressure when producers are faster than the consuming analyzer.\n *\n * @param analyzerPool: A pool of analyzers to use in this loop.\n * @param resultHandler: A result handler that will be called with the results from the analyzers in this loop.\n * @param analyzerLoopErrorListener: An error handler for this loop\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass ProcessBoundAnalyzerLoop<DataFrame, State, Output>(\n    private val analyzerPool: AnalyzerPool<DataFrame, in State, Output>,\n    private val resultHandler: StatefulResultHandler<DataFrame, out State, Output, Boolean>,\n    analyzerLoopErrorListener: AnalyzerLoopErrorListener\n) : AnalyzerLoop<DataFrame, State, Output>(\n    analyzerPool,\n    analyzerLoopErrorListener,\n) {\n    /**\n     * Subscribe to a flow. Loops can only subscribe to a single flow at a time.\n     */\n    fun subscribeTo(flow: Flow<DataFrame>, processingCoroutineScope: CoroutineScope) =\n        subscribeToFlow(flow, processingCoroutineScope)\n\n    /**\n     * Unsubscribe from the flow.\n     */\n    fun unsubscribe() = runBlocking { unsubscribeFromFlow() }\n\n    override suspend fun onResult(result: Output, data: DataFrame) = resultHandler.onResult(result, data)\n\n    override fun getState(): State = resultHandler.state\n}\n\n/**\n * This kind of [AnalyzerLoop] will process data provided as part of its constructor. Data will be processed in the\n * order provided.\n *\n * @param analyzerPool: A pool of analyzers to use in this loop.\n * @param resultHandler: A result handler that will be called with the results from the analyzers in this loop.\n * @param analyzerLoopErrorListener: An error handler for this loop\n * @param timeLimit: If specified, this is the maximum allowed time for the loop to run. If the loop\n *     exceeds this duration, the loop will terminate\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass FiniteAnalyzerLoop<DataFrame, State, Output>(\n    private val analyzerPool: AnalyzerPool<DataFrame, in State, Output>,\n    private val resultHandler: TerminatingResultHandler<DataFrame, out State, Output>,\n    analyzerLoopErrorListener: AnalyzerLoopErrorListener,\n    private val timeLimit: Duration = Duration.INFINITE\n) : AnalyzerLoop<DataFrame, State, Output>(\n    analyzerPool,\n    analyzerLoopErrorListener,\n) {\n    private val framesProcessed: AtomicInteger = AtomicInteger(0)\n    private var framesToProcess = 0\n\n    fun process(frames: Collection<DataFrame>, processingCoroutineScope: CoroutineScope): Job? {\n        val channel = Channel<DataFrame>(capacity = frames.size)\n        // TODO: upgrade this when kotlin libs hit 1.5.0\n//        framesToProcess = frames.map { channel.trySend(it) }.count { it.isSuccess }\n        @Suppress(\"DEPRECATION\")\n        framesToProcess = frames.map { channel.offer(it) }.count { it }\n        return if (framesToProcess > 0) {\n            subscribeToFlow(channel.receiveAsFlow(), processingCoroutineScope)\n        } else {\n            processingCoroutineScope.launch { resultHandler.onAllDataProcessed() }\n        }\n    }\n\n    fun cancel() = runBlocking { unsubscribeFromFlow() }\n\n    override suspend fun onResult(result: Output, data: DataFrame): Boolean {\n        val framesProcessed = this.framesProcessed.incrementAndGet()\n        val timeElapsed = startedAt?.elapsedSince() ?: Duration.ZERO\n        resultHandler.onResult(result, data)\n\n        if (framesProcessed >= framesToProcess) {\n            resultHandler.onAllDataProcessed()\n            unsubscribeFromFlow()\n        } else if (timeElapsed > timeLimit) {\n            resultHandler.onTerminatedEarly()\n            unsubscribeFromFlow()\n        }\n\n        val allFramesProcessed = framesProcessed >= framesToProcess\n        val exceededTimeLimit = timeElapsed > timeLimit\n        return allFramesProcessed || exceededTimeLimit\n    }\n\n    override fun getState(): State = resultHandler.state\n}\n\n/**\n * Consume this [Flow] using a channelFlow with no buffer. Elements emitted from [this] flow are offered to the\n * underlying [channelFlow]. If the consumer is not currently suspended and waiting for the next element, the element is\n * dropped.\n *\n * example:\n * ```\n * flow {\n *   (0..100).forEach {\n *     emit(it)\n *     delay(100)\n *   }\n * }.backPressureDrop().collect {\n *   delay(1000)\n *   println(it)\n * }\n * ```\n *\n * @return a flow that only emits elements when the downstream [Flow.collect] is waiting for the next element\n */\n@ExperimentalCoroutinesApi\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nsuspend fun <T> Flow<T>.backPressureDrop(): Flow<T> =\n    // TODO: upgrade this when kotlin libs hit 1.5.0\n//    channelFlow { this@backPressureDrop.collect { trySend(it) } }.buffer(capacity = Channel.RENDEZVOUS)\n    @Suppress(\"DEPRECATION\")\n    channelFlow { this@backPressureDrop.collect { offer(it) } }.buffer(capacity = Channel.RENDEZVOUS)\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/MachineState.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport android.util.Log\nimport com.getbouncer.scan.framework.time.Clock\nimport com.getbouncer.scan.framework.time.ClockMark\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nabstract class MachineState {\n\n    /**\n     * Keep track of when this state was reached\n     */\n    protected open val reachedStateAt: ClockMark = Clock.markNow()\n\n    override fun toString(): String = \"${this::class.java.simpleName}(reachedStateAt=$reachedStateAt)\"\n\n    init {\n        if (Config.isDebug) Log.d(Config.logTag, \"${this::class.java.simpleName} machine state reached\")\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/Result.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleObserver\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.OnLifecycleEvent\nimport com.getbouncer.scan.framework.util.FrameRateTracker\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.withContext\n\n/**\n * A result handler for data processing. This is called when results are available from an [Analyzer].\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ninterface ResultHandler<Input, Output, Verdict> {\n    suspend fun onResult(result: Output, data: Input): Verdict\n}\n\n/**\n * A specialized result handler that has some form of state.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nabstract class StatefulResultHandler<Input, State, Output, Verdict>(\n    private var initialState: State\n) : ResultHandler<Input, Output, Verdict> {\n\n    /**\n     * The state of the result handler. This can be read, but not updated by analyzers.\n     */\n    var state: State = initialState\n        protected set\n\n    /**\n     * Reset the state to the initial value.\n     */\n    protected open fun reset() { state = initialState }\n}\n\n/**\n * A result handler with a method that notifies when all data has been processed.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nabstract class TerminatingResultHandler<Input, State, Output>(\n    initialState: State\n) : StatefulResultHandler<Input, State, Output, Unit>(initialState) {\n    /**\n     * All data has been processed and termination was reached.\n     */\n    abstract suspend fun onAllDataProcessed()\n\n    /**\n     * Not all data was processed before termination.\n     */\n    abstract suspend fun onTerminatedEarly()\n}\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ninterface AggregateResultListener<InterimResult, FinalResult> {\n\n    /**\n     * The aggregated result of an [AnalyzerLoop] is available.\n     *\n     * @param result: the result from the Aggregator\n     */\n    suspend fun onResult(result: FinalResult)\n\n    /**\n     * An interim result is available, but the [AnalyzerLoop] is still processing more data frames. This is useful for\n     * displaying a debug window or handling state updates during a scan.\n     *\n     * @param result: the result from the [AnalyzerLoop]\n     */\n    suspend fun onInterimResult(result: InterimResult)\n\n    /**\n     * The result aggregator was reset back to its original state.\n     */\n    suspend fun onReset()\n}\n\n/**\n * The [ResultAggregator] processes results from analyzers until a condition is met. That condition is part of the\n * aggregator's logic.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nabstract class ResultAggregator<DataFrame, State, AnalyzerResult, InterimResult, FinalResult>(\n    private val listener: AggregateResultListener<InterimResult, FinalResult>,\n    private val initialState: State\n) : StatefulResultHandler<DataFrame, State, AnalyzerResult, Boolean>(initialState), LifecycleObserver {\n    private var isCanceled = false\n    private var isPaused = false\n    private var isFinished = false\n\n    private val aggregatorExecutionStats = runBlocking {\n        Stats.trackRepeatingTask(\"${this@ResultAggregator::class.java.simpleName}_aggregator_execution\")\n    }\n\n    protected open val frameRateTracker by lazy { FrameRateTracker(this::class.java.simpleName) }\n\n    /**\n     * Reset the state of the aggregator and pause aggregation. This is useful for aggregators that can be backgrounded.\n     * For example, a user that is scanning an object, but then backgrounds the scanning app. In the case that the scan\n     * should be restarted, this feature pauses the result handlers and resets the state.\n     */\n    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)\n    private fun resetAndPause() {\n        reset()\n        isPaused = true\n    }\n\n    /**\n     * Resume aggregation after it has been paused.\n     */\n    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)\n    private fun resume() {\n        isPaused = false\n    }\n\n    /**\n     * Cancel a result aggregator. This means that the result aggregator will ignore all further results and will never\n     * return a final result.\n     */\n    fun cancel() {\n        reset()\n        isCanceled = true\n    }\n\n    /**\n     * Bind this result aggregator to a lifecycle. This allows the result aggregator to pause and reset when the\n     * lifecycle owner pauses.\n     */\n    open fun bindToLifecycle(lifecycleOwner: LifecycleOwner) {\n        lifecycleOwner.lifecycle.addObserver(this)\n    }\n\n    /**\n     * Reset the state of the aggregator. This is useful for aggregating data that can become invalid, such as when a\n     * user is scanning an object, and moves the object away from the camera before the scan has completed.\n     */\n    override fun reset() {\n        super.reset()\n        isPaused = false\n        isCanceled = false\n        isFinished = false\n\n        state = initialState\n\n        frameRateTracker.reset()\n        runBlocking { listener.onReset() }\n    }\n\n    override suspend fun onResult(result: AnalyzerResult, data: DataFrame): Boolean = when {\n        isPaused -> false\n        isCanceled || isFinished -> true\n        else -> withContext(Dispatchers.Default) {\n            frameRateTracker.trackFrameProcessed()\n\n            val (interimResult, finalResult) = aggregateResult(data, result)\n\n            launch { listener.onInterimResult(interimResult) }\n\n            aggregatorExecutionStats.trackResult(\"frame_processed\")\n\n            finalResult?.also {\n                isFinished = true\n                launch { listener.onResult(it) }\n            } != null\n        }\n    }\n\n    /**\n     * Aggregate a new result. If this method returns a non-null [FinalResult], the aggregator will stop listening for\n     * new results.\n     *\n     * @param result: The result to aggregate\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    abstract suspend fun aggregateResult(frame: DataFrame, result: AnalyzerResult): Pair<InterimResult, FinalResult?>\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/Scan.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport android.os.Build\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nobject Scan {\n\n    /**\n     * Determine if the device is running an ARM architecture.\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun isDeviceArchitectureArm(): Boolean {\n        val arch = System.getProperty(\"os.arch\") ?: \"\"\n        return \"86\" !in arch\n    }\n\n    /**\n     * Determine the architecture of the device.\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun getDeviceArchitecture(): String? {\n        // From https://stackoverflow.com/questions/11989629/api-call-to-get-processor-architecture\n\n        // Note that we cannot use System.getProperty(\"os.arch\") since that may give e.g. \"aarch64\"\n        // while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo).\n        // Instead we search through the supported abi:s on the device, see:\n        // http://developer.android.com/ndk/guides/abis.html\n\n        // Note that we search for abi:s in preferred order (the ordering of the\n        // Build.SUPPORTED_ABIS list) to avoid e.g. installing arm on an x86 system where arm\n        // emulation is available.\n        for (androidArch in Build.SUPPORTED_ABIS) {\n            when (androidArch) {\n                \"arm64-v8a\" -> return \"aarch64\"\n                \"armeabi-v7a\" -> return \"arm\"\n                \"x86_64\" -> return \"x86_64\"\n                \"x86\" -> return \"i686\"\n            }\n        }\n        return null\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/Stat.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport android.util.Log\nimport androidx.annotation.CheckResult\nimport com.getbouncer.scan.framework.time.Clock\nimport com.getbouncer.scan.framework.time.ClockMark\nimport com.getbouncer.scan.framework.time.Duration\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.supervisorScope\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport java.util.UUID\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nobject Stats {\n    val instanceId = UUID.randomUUID().toString()\n\n    var scanId: String? = null\n        private set\n\n    private var persistentRepeatingTasks: MutableMap<String, MutableMap<String, RepeatingTaskStats>> = mutableMapOf()\n    private var tasks: MutableMap<String, List<TaskStats>> = mutableMapOf()\n    private var repeatingTasks: MutableMap<String, MutableMap<String, RepeatingTaskStats>> = mutableMapOf()\n\n    private val scanIdMutex = Mutex()\n    private val taskMutex = Mutex()\n    private val repeatingTaskMutex = Mutex()\n    private val persistentRepeatingTasksMutex = Mutex()\n\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun startScan() {\n        runBlocking {\n            scanIdMutex.withLock {\n                clearAllTasks()\n                scanId = UUID.randomUUID().toString()\n            }\n        }\n    }\n\n    /**\n     * Track the duration of a task.\n     */\n    @CheckResult\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun trackTask(name: String): StatTracker =\n        if (!Config.trackStats) StatTrackerNoOpImpl else StatTrackerImpl { startedAt, result ->\n            taskMutex.withLock {\n                val list = tasks[name]\n                if (list == null) {\n                    tasks[name] = listOf(TaskStats(startedAt, startedAt.elapsedSince(), result))\n                } else {\n                    tasks[name] = list + TaskStats(startedAt, startedAt.elapsedSince(), result)\n                }\n            }\n            if (Config.isDebug) {\n                Log.v(Config.logTag, \"Task $name got result $result after ${startedAt.elapsedSince()}\")\n            }\n        }\n\n    /**\n     * Track the result of a task.\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    suspend fun <T> trackTask(name: String, task: suspend () -> T): T {\n        val tracker = trackTask(name)\n        val result: T\n        try {\n            result = task()\n            tracker.trackResult(\"success\")\n        } catch (t: Throwable) {\n            tracker.trackResult(t::class.java.simpleName)\n            throw t\n        }\n\n        return result\n    }\n\n    /**\n     * Track a single execution of a repeating task.\n     */\n    @CheckResult\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun trackRepeatingTask(name: String): StatTracker =\n        if (!Config.trackStats) StatTrackerNoOpImpl else StatTrackerImpl { startedAt, result ->\n            repeatingTaskMutex.withLock {\n                val resultName = result ?: \"null\"\n                val resultStats = repeatingTasks[name] ?: run {\n                    val taskStats = mutableMapOf<String, RepeatingTaskStats>()\n                    repeatingTasks[name] = taskStats\n                    taskStats\n                }\n\n                val taskStats = resultStats[resultName]\n                val duration = startedAt.elapsedSince()\n                if (taskStats == null) {\n                    resultStats[resultName] = RepeatingTaskStats(\n                        executions = 1,\n                        startedAt = startedAt,\n                        totalDuration = duration,\n                        totalCpuDuration = duration,\n                        minimumDuration = duration,\n                        maximumDuration = duration,\n                    )\n                } else {\n                    resultStats[resultName] = RepeatingTaskStats(\n                        executions = taskStats.executions + 1,\n                        startedAt = taskStats.startedAt,\n                        totalDuration = taskStats.startedAt.elapsedSince(),\n                        totalCpuDuration = taskStats.totalCpuDuration + duration,\n                        minimumDuration = minOf(taskStats.minimumDuration, duration),\n                        maximumDuration = maxOf(taskStats.maximumDuration, duration),\n                    )\n                }\n            }\n            if (Config.isDebug) {\n                Log.v(Config.logTag, \"Repeating task $name got result $result after ${startedAt.elapsedSince()}\")\n            }\n        }\n\n    /**\n     * Track a single execution of a repeating task that should not be cleared on scan start.\n     */\n    @CheckResult\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun trackPersistentRepeatingTask(name: String): StatTracker =\n        if (!Config.trackStats) StatTrackerNoOpImpl else StatTrackerImpl { startedAt, result ->\n            persistentRepeatingTasksMutex.withLock {\n                val resultName = result ?: \"null\"\n                val resultStats = persistentRepeatingTasks[name] ?: run {\n                    val taskStats = mutableMapOf<String, RepeatingTaskStats>()\n                    persistentRepeatingTasks[name] = taskStats\n                    taskStats\n                }\n\n                val taskStats = resultStats[resultName]\n                val duration = startedAt.elapsedSince()\n                if (taskStats == null) {\n                    resultStats[resultName] = RepeatingTaskStats(\n                        executions = 1,\n                        startedAt = startedAt,\n                        totalDuration = duration,\n                        totalCpuDuration = duration,\n                        minimumDuration = duration,\n                        maximumDuration = duration,\n                    )\n                } else {\n                    resultStats[resultName] = RepeatingTaskStats(\n                        executions = taskStats.executions + 1,\n                        startedAt = taskStats.startedAt,\n                        totalDuration = taskStats.startedAt.elapsedSince(),\n                        totalCpuDuration = taskStats.totalCpuDuration + duration,\n                        minimumDuration = minOf(taskStats.minimumDuration, duration),\n                        maximumDuration = maxOf(taskStats.maximumDuration, duration),\n                    )\n                }\n            }\n            if (Config.isDebug) {\n                Log.v(Config.logTag, \"Persistent repeating task $name got result $result after ${startedAt.elapsedSince()}\")\n            }\n        }\n\n    /**\n     * Track the result of a task.\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    suspend fun <T> trackRepeatingTask(name: String, task: () -> T): T {\n        val tracker = trackRepeatingTask(name)\n        val result: T\n        try {\n            result = task()\n            tracker.trackResult(\"success\")\n        } catch (t: Throwable) {\n            tracker.trackResult(t::class.java.simpleName)\n            throw t\n        }\n\n        return result\n    }\n\n    @JvmStatic\n    @CheckResult\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun getRepeatingTasks() = runBlocking {\n        repeatingTaskMutex.withLock {\n            persistentRepeatingTasksMutex.withLock {\n                repeatingTasks.toMap().mapValues { entry -> entry.value.toMap() } +\n                    persistentRepeatingTasks.toMap().mapValues { entry -> entry.value.toMap() }\n            }\n        }\n    }\n\n    @JvmStatic\n    @CheckResult\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun getTasks() = runBlocking {\n        taskMutex.withLock { tasks.toMap() }\n    }\n\n    private suspend fun clearAllTasks() = supervisorScope {\n        val tasksAsync = async { taskMutex.withLock { tasks.clear() } }\n        val repeatingTasksAsync = async { repeatingTaskMutex.withLock { repeatingTasks.clear() } }\n\n        tasksAsync.await()\n        repeatingTasksAsync.await()\n    }\n}\n\n/**\n * Keep track of a single stat's duration and result\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ninterface StatTracker {\n\n    /**\n     * When this task was started.\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    val startedAt: ClockMark\n\n    /**\n     * Track the result from a stat.\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    suspend fun trackResult(result: String? = null)\n}\n\nprivate object StatTrackerNoOpImpl : StatTracker {\n    override val startedAt = Clock.markNow()\n    override suspend fun trackResult(result: String?) { /* do nothing */ }\n}\n\nprivate class StatTrackerImpl(private val onComplete: suspend (ClockMark, String?) -> Unit) : StatTracker {\n    override val startedAt = Clock.markNow()\n    override suspend fun trackResult(result: String?) = coroutineScope { launch { onComplete(startedAt, result) } }.let { Unit }\n}\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class TaskStats(\n    val started: ClockMark,\n    val duration: Duration,\n    val result: String?,\n)\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class RepeatingTaskStats(\n    val executions: Int,\n    val startedAt: ClockMark,\n    val totalDuration: Duration,\n    val totalCpuDuration: Duration,\n    val minimumDuration: Duration,\n    val maximumDuration: Duration,\n) {\n    fun averageDuration() = totalCpuDuration / executions\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/Storage.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport android.util.Log\n\nprivate const val STORAGE_FILE_NAME = \"bouncer_shared_prefs\"\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ninterface Storage {\n\n    /**\n     * Store a String in app storage by a [key].\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun storeValue(key: String, value: String): Boolean\n\n    /**\n     * Store a Long in app storage by a [key].\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun storeValue(key: String, value: Long): Boolean\n\n    /**\n     * Store an Int in app storage by a [key].\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun storeValue(key: String, value: Int): Boolean\n\n    /**\n     * Store a Float in app storage by a [key].\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun storeValue(key: String, value: Float): Boolean\n\n    /**\n     * Store a Boolean in app storage by a [key].\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun storeValue(key: String, value: Boolean): Boolean\n\n    /**\n     * Retrieve a String from app storage by a [key].\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun getString(key: String, defaultValue: String): String\n\n    /**\n     * Retrieve a Long from app storage by a [key].\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun getLong(key: String, defaultValue: Long): Long\n\n    /**\n     * Retrieve an Int from app storage by a [key].\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun getInt(key: String, defaultValue: Int): Int\n\n    /**\n     * Retrieve a Float from app storage by a [key].\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun getFloat(key: String, defaultValue: Float): Float\n\n    /**\n     * Retrieve a Boolean from app storage by a [key].\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun getBoolean(key: String, defaultValue: Boolean): Boolean\n\n    /**\n     * Clears out a single value from storage.\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun remove(key: String): Boolean\n\n    /**\n     * Clear out all values from storage.\n     */\n    @Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\n    fun clear(): Boolean\n}\n\n/**\n * A class that handles access to storage.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nobject StorageFactory {\n    fun getStorageInstance(context: Context, purpose: String): Storage =\n        SharedPreferencesStorage(context.applicationContext, purpose)\n}\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass SharedPreferencesStorage(private val context: Context, private val purpose: String) : Storage {\n    private val sharedPrefs: SharedPreferences? by lazy {\n        context.getSharedPreferences(STORAGE_FILE_NAME, Context.MODE_PRIVATE)\n    }\n\n    override fun storeValue(key: String, value: String) = sharedPrefs?.run {\n        with(edit()) {\n            putString(\"${purpose}_$key\", value)\n            commit()\n        }\n    } ?: false.apply {\n        Log.e(Config.logTag, \"Shared preferences is unavailable to store $value for $key\")\n    }\n\n    override fun storeValue(key: String, value: Long) = sharedPrefs?.run {\n        with(edit()) {\n            putLong(\"${purpose}_$key\", value)\n            commit()\n        }\n    } ?: false.apply {\n        Log.e(Config.logTag, \"Shared preferences is unavailable to store $value for $key\")\n    }\n\n    override fun storeValue(key: String, value: Int) = sharedPrefs?.run {\n        with(edit()) {\n            putInt(\"${purpose}_$key\", value)\n            commit()\n        }\n    } ?: false.apply {\n        Log.e(Config.logTag, \"Shared preferences is unavailable to store $value for $key\")\n    }\n\n    override fun storeValue(key: String, value: Float) = sharedPrefs?.run {\n        with(edit()) {\n            putFloat(\"${purpose}_$key\", value)\n            commit()\n        }\n    } ?: false.apply {\n        Log.e(Config.logTag, \"Shared preferences is unavailable to store $value for $key\")\n    }\n\n    override fun storeValue(key: String, value: Boolean) = sharedPrefs?.run {\n        with(edit()) {\n            putBoolean(\"${purpose}_$key\", value)\n            commit()\n        }\n    } ?: false.apply {\n        Log.e(Config.logTag, \"Shared preferences is unavailable to store $value for $key\")\n    }\n\n    override fun getString(key: String, defaultValue: String): String {\n        return try {\n            sharedPrefs?.getString(\"${purpose}_$key\", defaultValue) ?: defaultValue.apply {\n                Log.e(Config.logTag, \"Shared preferences is unavailable to retrieve a String for $key\")\n            }\n        } catch (t: Throwable) {\n            when (t) {\n                is ClassCastException -> Log.e(Config.logTag, \"Attempted to read String, but $key is not a String\", t)\n                else -> Log.d(Config.logTag, \"Error retrieving String for $key\", t)\n            }\n            defaultValue\n        }\n    }\n\n    override fun getLong(key: String, defaultValue: Long): Long {\n        return try {\n            sharedPrefs?.getLong(\"${purpose}_$key\", defaultValue) ?: defaultValue.apply {\n                Log.e(Config.logTag, \"Shared preferences is unavailable to retrieve a Long for $key\")\n            }\n        } catch (t: Throwable) {\n            when (t) {\n                is ClassCastException -> Log.e(Config.logTag, \"Attempted to read Long, but $key is not a Long\", t)\n                else -> Log.d(Config.logTag, \"Error retrieving Long for $key\", t)\n            }\n            defaultValue\n        }\n    }\n\n    override fun getInt(key: String, defaultValue: Int): Int {\n        return try {\n            sharedPrefs?.getInt(\"${purpose}_$key\", defaultValue) ?: defaultValue.apply {\n                Log.e(Config.logTag, \"Shared preferences is unavailable to retrieve an Int for $key\")\n            }\n        } catch (t: Throwable) {\n            when (t) {\n                is ClassCastException -> Log.e(Config.logTag, \"Attempted to read Int, but $key is not a Int\", t)\n                else -> Log.d(Config.logTag, \"Error retrieving Int for $key\", t)\n            }\n            defaultValue\n        }\n    }\n\n    override fun getFloat(key: String, defaultValue: Float): Float {\n        return try {\n            sharedPrefs?.getFloat(\"${purpose}_$key\", defaultValue) ?: defaultValue.apply {\n                Log.e(Config.logTag, \"Shared preferences is unavailable to retrieve a Float for $key\")\n            }\n        } catch (t: Throwable) {\n            when (t) {\n                is ClassCastException -> Log.e(Config.logTag, \"Attempted to read Float, but $key is not a Float\", t)\n                else -> Log.d(Config.logTag, \"Error retrieving Float for $key\", t)\n            }\n            defaultValue\n        }\n    }\n\n    override fun getBoolean(key: String, defaultValue: Boolean): Boolean {\n        return try {\n            sharedPrefs?.getBoolean(\"${purpose}_$key\", defaultValue) ?: defaultValue.apply {\n                Log.e(Config.logTag, \"Shared preferences is unavailable to retrieve a Boolean for $key\")\n            }\n        } catch (t: Throwable) {\n            when (t) {\n                is ClassCastException -> Log.e(Config.logTag, \"Attempted to read Boolean, but $key is not a Boolean\", t)\n                else -> Log.d(Config.logTag, \"Error retrieving Boolean for $key\", t)\n            }\n            defaultValue\n        }\n    }\n\n    override fun remove(key: String): Boolean = sharedPrefs?.run {\n        with(edit()) {\n            remove(key)\n            commit()\n        }\n    } ?: false.apply {\n        Log.e(Config.logTag, \"Shared preferences is unavailable to remove values\")\n    }\n\n    override fun clear(): Boolean = sharedPrefs?.run {\n        with(edit()) {\n            clear()\n            commit()\n        }\n    } ?: false.apply {\n        Log.e(Config.logTag, \"Shared preferences is unavailable to clear values\")\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/TrackedImage.kt",
    "content": "package com.getbouncer.scan.framework\n\n/**\n * An image with a stat tracker.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class TrackedImage<ImageType>(\n    val image: ImageType,\n    val tracker: StatTracker,\n)\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/api/BouncerApi.kt",
    "content": "@file:JvmName(\"BouncerApi\")\npackage com.getbouncer.scan.framework.api\n\nimport android.content.Context\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.api.dto.AppInfo\nimport com.getbouncer.scan.framework.api.dto.BouncerErrorResponse\nimport com.getbouncer.scan.framework.api.dto.ClientDevice\nimport com.getbouncer.scan.framework.api.dto.ModelDetailsRequest\nimport com.getbouncer.scan.framework.api.dto.ModelDetailsResponse\nimport com.getbouncer.scan.framework.api.dto.ModelSignedUrlResponse\nimport com.getbouncer.scan.framework.api.dto.ModelVersion\nimport com.getbouncer.scan.framework.api.dto.ScanStatistics\nimport com.getbouncer.scan.framework.api.dto.StatsPayload\nimport com.getbouncer.scan.framework.api.dto.ValidateApiKeyResponse\nimport com.getbouncer.scan.framework.ml.getLoadedModelVersions\nimport com.getbouncer.scan.framework.util.AppDetails\nimport com.getbouncer.scan.framework.util.Device\nimport com.getbouncer.scan.framework.util.getPlatform\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\nprivate const val STATS_PATH = \"/scan_stats\"\nprivate const val API_KEY_VALIDATION_PATH = \"/v1/api_key/validate\"\nprivate const val MODEL_SIGNED_URL_PATH = \"/v1/signed_url/model/%s/%s/android/%s\"\nprivate const val MODEL_DETAILS_PATH = \"/v2/model_details\"\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nconst val ERROR_CODE_NOT_AUTHENTICATED = \"not_authenticated\"\n\n/**\n * Upload stats data to bouncer servers.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun uploadScanStats(\n    context: Context,\n    instanceId: String,\n    scanId: String?,\n    device: Device,\n    appDetails: AppDetails,\n    scanStatistics: ScanStatistics,\n) = GlobalScope.launch(Dispatchers.IO) {\n    postData(\n        context = context.applicationContext,\n        path = STATS_PATH,\n        data = StatsPayload(\n            instanceId = instanceId,\n            scanId = scanId,\n            device = ClientDevice.fromDevice(device),\n            app = AppInfo.fromAppDetails(appDetails),\n            scanStats = scanStatistics,\n            modelVersions = getLoadedModelVersions().map { ModelVersion.fromModelLoadDetails(it) },\n        ),\n        requestSerializer = StatsPayload.serializer(),\n    )\n}\n\n/**\n * Validate an API key.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsuspend fun validateApiKey(context: Context): NetworkResult<out ValidateApiKeyResponse, out BouncerErrorResponse> =\n    withContext(Dispatchers.IO) {\n        getForResult(\n            context = context,\n            path = API_KEY_VALIDATION_PATH,\n            responseSerializer = ValidateApiKeyResponse.serializer(),\n            errorSerializer = BouncerErrorResponse.serializer(),\n        )\n    }\n\n/**\n * Get a signed URL for a model.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsuspend fun getModelSignedUrl(\n    context: Context,\n    modelClass: String,\n    modelVersion: String,\n    modelFileName: String,\n): NetworkResult<out ModelSignedUrlResponse, out BouncerErrorResponse> =\n    withContext(Dispatchers.IO) {\n        getForResult(\n            context = context,\n            path = MODEL_SIGNED_URL_PATH.format(modelClass, modelVersion, modelFileName),\n            responseSerializer = ModelSignedUrlResponse.serializer(),\n            errorSerializer = BouncerErrorResponse.serializer(),\n        )\n    }\n\n/**\n * Get details about a model.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsuspend fun getModelDetails(\n    context: Context,\n    modelClass: String,\n    modelFrameworkVersion: Int,\n    cachedModelHash: String?,\n    cachedModelHashAlgorithm: String?,\n): NetworkResult<out ModelDetailsResponse, out BouncerErrorResponse> =\n    withContext(Dispatchers.IO) {\n        postForResult(\n            context = context,\n            path = MODEL_DETAILS_PATH,\n            requestSerializer = ModelDetailsRequest.serializer(),\n            responseSerializer = ModelDetailsResponse.serializer(),\n            errorSerializer = BouncerErrorResponse.serializer(),\n            data = ModelDetailsRequest(\n                platform = getPlatform(),\n                modelClass = modelClass,\n                modelFrameworkVersion = modelFrameworkVersion,\n                cachedModelHash = cachedModelHash,\n                cachedModelHashAlgorithm = cachedModelHashAlgorithm,\n                betaOptIn = Config.betaModelOptIn,\n            ),\n        )\n    }\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/api/Network.kt",
    "content": "@file:JvmName(\"Network\")\npackage com.getbouncer.scan.framework.api\n\nimport android.content.Context\nimport android.util.Base64\nimport android.util.Log\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.NetworkConfig\nimport com.getbouncer.scan.framework.time.Timer\nimport com.getbouncer.scan.framework.util.DeviceIds\nimport com.getbouncer.scan.framework.util.cacheFirstResult\nimport com.getbouncer.scan.framework.util.getAppPackageName\nimport com.getbouncer.scan.framework.util.getDeviceName\nimport com.getbouncer.scan.framework.util.getOsVersion\nimport com.getbouncer.scan.framework.util.getPlatform\nimport com.getbouncer.scan.framework.util.getSdkFlavor\nimport com.getbouncer.scan.framework.util.getSdkVersion\nimport com.getbouncer.scan.framework.util.retry\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.Serializable\nimport java.io.File\nimport java.io.FileNotFoundException\nimport java.io.FileOutputStream\nimport java.io.IOException\nimport java.io.InputStreamReader\nimport java.io.OutputStream\nimport java.io.OutputStreamWriter\nimport java.net.HttpURLConnection\nimport java.net.URL\nimport java.util.zip.GZIPOutputStream\n\nprivate const val REQUEST_METHOD_GET = \"GET\"\nprivate const val REQUEST_METHOD_POST = \"POST\"\n\nprivate const val REQUEST_PROPERTY_AUTHENTICATION = \"x-bouncer-auth\"\nprivate const val REQUEST_PROPERTY_DEVICE_ID = \"x-bouncer-device-id\"\nprivate const val REQUEST_PROPERTY_USER_AGENT = \"User-Agent\"\nprivate const val REQUEST_PROPERTY_CONTENT_TYPE = \"Content-Type\"\nprivate const val REQUEST_PROPERTY_CONTENT_ENCODING = \"Content-Encoding\"\n\nprivate const val CONTENT_TYPE_JSON = \"application/json; utf-8\"\nprivate const val CONTENT_ENCODING_GZIP = \"gzip\"\n\n/**\n * The size of a TCP network packet. If smaller than this, there is no benefit to GZIP.\n */\nprivate const val GZIP_MIN_SIZE_BYTES = 1500\n\nprivate val networkTimer by lazy { Timer.newInstance(Config.logTag, \"network\") }\n\n/**\n * Send a post request to a bouncer endpoint.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsuspend fun <Request, Response, Error> postForResult(\n    context: Context,\n    path: String,\n    data: Request,\n    requestSerializer: KSerializer<Request>,\n    responseSerializer: KSerializer<Response>,\n    errorSerializer: KSerializer<Error>\n): NetworkResult<out Response, out Error> =\n    translateNetworkResult(\n        networkResult = postJsonWithRetries(\n            context = context,\n            path = path,\n            jsonData = Config.json.encodeToString(requestSerializer, data)\n        ),\n        responseSerializer = responseSerializer,\n        errorSerializer = errorSerializer\n    )\n\n/**\n * Send a post request to a bouncer endpoint and ignore the response.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsuspend fun <Request> postData(\n    context: Context,\n    path: String,\n    data: Request,\n    requestSerializer: KSerializer<Request>\n) {\n    postJsonWithRetries(\n        context = context,\n        path = path,\n        jsonData = Config.json.encodeToString(requestSerializer, data)\n    )\n}\n\n/**\n * Send a get request to a bouncer endpoint and parse the response.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsuspend fun <Response, Error> getForResult(\n    context: Context,\n    path: String,\n    responseSerializer: KSerializer<Response>,\n    errorSerializer: KSerializer<Error>\n): NetworkResult<out Response, out Error> =\n    translateNetworkResult(getWithRetries(context, path), responseSerializer, errorSerializer)\n\n/**\n * Translate a string network result to a response or error.\n */\nprivate fun <Response, Error> translateNetworkResult(\n    networkResult: NetworkResult<out String, out String>,\n    responseSerializer: KSerializer<Response>,\n    errorSerializer: KSerializer<Error>\n): NetworkResult<out Response, out Error> = when (networkResult) {\n    is NetworkResult.Success ->\n        try {\n            NetworkResult.Success(\n                responseCode = networkResult.responseCode,\n                body = Config.json.decodeFromString(responseSerializer, networkResult.body)\n            )\n        } catch (t: Throwable) {\n            try {\n                NetworkResult.Error(\n                    responseCode = networkResult.responseCode,\n                    error = Config.json.decodeFromString(errorSerializer, networkResult.body)\n                )\n            } catch (et: Throwable) {\n                NetworkResult.Exception(networkResult.responseCode, t)\n            }\n        }\n    is NetworkResult.Error ->\n        try {\n            NetworkResult.Error(\n                responseCode = networkResult.responseCode,\n                error = Config.json.decodeFromString(errorSerializer, networkResult.error)\n            )\n        } catch (t: Throwable) {\n            NetworkResult.Exception(networkResult.responseCode, t)\n        }\n    is NetworkResult.Exception ->\n        NetworkResult.Exception(\n            responseCode = networkResult.responseCode,\n            exception = networkResult.exception\n        )\n}\n\n/**\n * Send a post request to a bouncer endpoint with retries.\n */\nprivate suspend fun postJsonWithRetries(\n    context: Context,\n    path: String,\n    jsonData: String\n): NetworkResult<out String, out String> =\n    try {\n        retry(\n            retryDelay = NetworkConfig.retryDelay,\n            times = NetworkConfig.retryTotalAttempts\n        ) {\n            val result = postJson(context, path, jsonData)\n            if (result.responseCode in NetworkConfig.retryStatusCodes) {\n                throw RetryNetworkRequestException(result)\n            } else {\n                result\n            }\n        }\n    } catch (e: RetryNetworkRequestException) {\n        e.result\n    }\n\n/**\n * Send a get request to a bouncer endpoint with retries.\n */\nprivate suspend fun getWithRetries(context: Context, path: String): NetworkResult<out String, out String> =\n    try {\n        retry(\n            retryDelay = NetworkConfig.retryDelay,\n            times = NetworkConfig.retryTotalAttempts\n        ) {\n            val result = get(context, path)\n            if (result.responseCode in NetworkConfig.retryStatusCodes) {\n                throw RetryNetworkRequestException(result)\n            } else {\n                result\n            }\n        }\n    } catch (e: RetryNetworkRequestException) {\n        e.result\n    }\n\n/**\n * Send a post request to a bouncer endpoint.\n */\nprivate fun postJson(\n    context: Context,\n    path: String,\n    jsonData: String\n): NetworkResult<out String, out String> = networkTimer.measure(path) {\n    val fullPath = if (path.startsWith(\"/\")) path else \"/$path\"\n    val url = URL(\"${getBaseUrl()}$fullPath\")\n    var responseCode = -1\n\n    try {\n        with(url.openConnection() as HttpURLConnection) {\n            requestMethod = REQUEST_METHOD_POST\n\n            // Set the connection to both send and receive data\n            doOutput = true\n            doInput = true\n\n            // Set headers\n            setRequestHeaders(context)\n            setRequestProperty(REQUEST_PROPERTY_CONTENT_TYPE, CONTENT_TYPE_JSON)\n\n            // Write the data\n            if (NetworkConfig.useCompression && jsonData.toByteArray().size >= GZIP_MIN_SIZE_BYTES) {\n                setRequestProperty(REQUEST_PROPERTY_CONTENT_ENCODING, CONTENT_ENCODING_GZIP)\n                writeGzipData(\n                    outputStream,\n                    jsonData\n                )\n            } else {\n                writeData(\n                    outputStream,\n                    jsonData\n                )\n            }\n\n            // Read the response code. This will block until the response has been received.\n            responseCode = this.responseCode\n\n            // Read the response\n            when (responseCode) {\n                in 200 until 300 -> NetworkResult.Success(\n                    responseCode,\n                    readResponse(this)\n                )\n                else -> NetworkResult.Error(\n                    responseCode,\n                    readResponse(this)\n                )\n            }\n        }\n    } catch (t: Throwable) {\n        Log.w(Config.logTag, \"Failed network request to endpoint $url\", t)\n        NetworkResult.Exception(responseCode, t)\n    }\n}\n\n/**\n * Send a get request to a bouncer endpoint.\n */\nprivate fun get(context: Context, path: String): NetworkResult<out String, out String> = networkTimer.measure(path) {\n    val fullPath = if (path.startsWith(\"/\")) path else \"/$path\"\n    val url = URL(\"${getBaseUrl()}$fullPath\")\n    var responseCode = -1\n\n    try {\n        with(url.openConnection() as HttpURLConnection) {\n            requestMethod = REQUEST_METHOD_GET\n\n            // Set the connection to only receive data\n            doOutput = false\n            doInput = true\n\n            // Set headers\n            setRequestHeaders(context)\n\n            // Read the response code. This will block until the response has been received.\n            responseCode = this.responseCode\n\n            // Read the response\n            when (responseCode) {\n                in 200 until 300 -> NetworkResult.Success(\n                    responseCode,\n                    readResponse(this)\n                )\n                else -> NetworkResult.Error(\n                    responseCode,\n                    readResponse(this)\n                )\n            }\n        }\n    } catch (t: Throwable) {\n        Log.w(Config.logTag, \"Failed network request to endpoint $url\", t)\n        NetworkResult.Exception(responseCode, t)\n    }\n}\n\n@Throws(IOException::class)\nsuspend fun downloadFileWithRetries(context: Context, url: URL, outputFile: File) = retry(\n    NetworkConfig.retryDelay,\n    excluding = listOf(FileNotFoundException::class.java)\n) {\n    downloadFile(context, url, outputFile)\n}\n\n/**\n * Download a file.\n */\n@Throws(IOException::class)\nprivate fun downloadFile(context: Context, url: URL, outputFile: File) = networkTimer.measure(url.toString()) {\n    try {\n        with(url.openConnection() as HttpURLConnection) {\n            requestMethod = REQUEST_METHOD_GET\n\n            // Set the connection to only receive data\n            doOutput = false\n            doInput = true\n\n            // set headers\n            setRequestHeaders(context)\n\n            // Read the response code. This will block until the response has been received.\n            val responseCode = this.responseCode\n\n            inputStream.use { stream ->\n                FileOutputStream(outputFile).use { stream.copyTo(it) }\n            }\n\n            responseCode\n        }\n    } catch (t: Throwable) {\n        Log.w(Config.logTag, \"Failed network request to endpoint $url\", t)\n        throw t\n    }\n}\n\n/**\n * Set the required request headers on an HttpURLConnection\n */\nprivate fun HttpURLConnection.setRequestHeaders(context: Context) {\n    setRequestProperty(REQUEST_PROPERTY_AUTHENTICATION, Config.apiKey)\n    setRequestProperty(REQUEST_PROPERTY_USER_AGENT, buildUserAgent(context))\n    setRequestProperty(REQUEST_PROPERTY_DEVICE_ID, buildDeviceId(context))\n}\n\n@Serializable\nprivate data class DeviceIdStructure(\n    /**\n     * android_id\n     */\n    val a: String,\n\n    /**\n     * vendor_id\n     */\n    val v: String,\n\n    /**\n     * advertising_id\n     */\n    val d: String\n)\n\nprivate val buildDeviceId = cacheFirstResult { context: Context ->\n    DeviceIds.fromContext(context).run {\n        Base64.encodeToString(\n            Config.json.encodeToString(\n                DeviceIdStructure.serializer(),\n                DeviceIdStructure(a = androidId ?: \"\", v = \"\", d = \"\")\n            ).toByteArray(Charsets.UTF_8),\n            Base64.URL_SAFE\n        )\n    }\n}\n\nprivate val buildUserAgent = cacheFirstResult { context: Context ->\n    \"bouncer/${getPlatform()}/${getAppPackageName(context)}/${getDeviceName()}/${getOsVersion()}/${getSdkVersion()}/${getSdkFlavor()}\"\n}\n\nprivate fun writeGzipData(outputStream: OutputStream, data: String) {\n    OutputStreamWriter(\n        GZIPOutputStream(\n            outputStream\n        )\n    ).use {\n        it.write(data)\n        it.flush()\n    }\n}\n\nprivate fun writeData(outputStream: OutputStream, data: String) {\n    OutputStreamWriter(outputStream).use {\n        it.write(data)\n        it.flush()\n    }\n}\n\nprivate fun readResponse(connection: HttpURLConnection): String =\n    InputStreamReader(connection.inputStream).use {\n        it.readLines().joinToString(separator = \"\\n\")\n    }\n\n/**\n * Get the [NetworkConfig.baseUrl] with no trailing slashes.\n */\nprivate fun getBaseUrl() = if (NetworkConfig.baseUrl.endsWith(\"/\")) {\n    NetworkConfig.baseUrl.substring(0, NetworkConfig.baseUrl.length - 1)\n} else {\n    NetworkConfig.baseUrl\n}\n\n/**\n * An exception that should never be thrown, but is required for typing.\n */\nprivate class RetryNetworkRequestException(val result: NetworkResult<out String, out String>) : Exception()\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/api/NetworkResult.kt",
    "content": "package com.getbouncer.scan.framework.api\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsealed class NetworkResult<Success, Error>(open val responseCode: Int) {\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Success<Success>(override val responseCode: Int, val body: Success) : NetworkResult<Success, Nothing>(responseCode)\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Error<Error>(override val responseCode: Int, val error: Error) : NetworkResult<Nothing, Error>(responseCode)\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Exception(override val responseCode: Int, val exception: Throwable) : NetworkResult<Nothing, Nothing>(responseCode)\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/AppInfo.kt",
    "content": "package com.getbouncer.scan.framework.api.dto\n\nimport androidx.annotation.RestrictTo\nimport com.getbouncer.scan.framework.util.AppDetails\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\n@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class AppInfo(\n    @SerialName(\"app_package_name\") val appPackageName: String?,\n    @SerialName(\"application_id\") val applicationId: String,\n    @SerialName(\"library_package_name\") val libraryPackageName: String,\n    @SerialName(\"sdk_version\") val sdkVersion: String,\n    @SerialName(\"sdk_version_code\") val sdkVersionCode: Int,\n    @SerialName(\"sdk_flavor\") val sdkFlavor: String,\n    @SerialName(\"is_debug_build\") val isDebugBuild: Boolean\n) {\n    companion object {\n        @Deprecated(\n            message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n            replaceWith = ReplaceWith(\"StripeCardScan\"),\n        )\n        fun fromAppDetails(appDetails: AppDetails): AppInfo = AppInfo(\n            appPackageName = appDetails.appPackageName,\n            applicationId = appDetails.applicationId,\n            libraryPackageName = appDetails.libraryPackageName,\n            sdkVersion = appDetails.sdkVersion,\n            sdkVersionCode = appDetails.sdkVersionCode,\n            sdkFlavor = appDetails.sdkFlavor,\n            isDebugBuild = appDetails.isDebugBuild\n        )\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/BouncerErrorResponse.kt",
    "content": "package com.getbouncer.scan.framework.api.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class BouncerErrorResponse(\n    @SerialName(\"status\") val status: String,\n    @SerialName(\"error_code\") val errorCode: String,\n    @SerialName(\"error_message\") val errorMessage: String,\n    @SerialName(\"error_payload\") val errorPayload: String?\n)\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/ClientDevice.kt",
    "content": "package com.getbouncer.scan.framework.api.dto\n\nimport androidx.annotation.RestrictTo\nimport com.getbouncer.scan.framework.util.Device\nimport com.getbouncer.scan.framework.util.DeviceIds\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\n@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class ClientDevice(\n    @SerialName(\"ids\") val ids: ClientDeviceIds,\n    @SerialName(\"type\") val name: String,\n    @SerialName(\"boot_count\") val bootCount: Int,\n    @SerialName(\"locale\") val locale: String?,\n    @SerialName(\"carrier\") val carrier: String?,\n    @SerialName(\"network_operator\") val networkOperator: String?,\n    @SerialName(\"phone_type\") val phoneType: Int?,\n    @SerialName(\"phone_count\") val phoneCount: Int,\n    @SerialName(\"os_version\") val osVersion: String,\n    @SerialName(\"platform\") val platform: String\n) {\n    companion object {\n        @Deprecated(\n            message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n            replaceWith = ReplaceWith(\"StripeCardScan\"),\n        )\n        fun fromDevice(device: Device) = ClientDevice(\n            ids = ClientDeviceIds.fromDeviceIds(device.ids),\n            name = device.name,\n            bootCount = device.bootCount,\n            locale = device.locale,\n            carrier = device.carrier,\n            networkOperator = device.networkOperator,\n            phoneType = device.phoneType,\n            phoneCount = device.phoneCount,\n            osVersion = device.osVersion.toString(),\n            platform = device.platform\n        )\n    }\n}\n\n@Serializable\n@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class ClientDeviceIds(\n    @SerialName(\"vendor_id\") val androidId: String?\n) {\n    companion object {\n        @Deprecated(\n            message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n            replaceWith = ReplaceWith(\"StripeCardScan\"),\n        )\n        fun fromDeviceIds(deviceIds: DeviceIds) = ClientDeviceIds(\n            androidId = deviceIds.androidId\n        )\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/ClientStats.kt",
    "content": "package com.getbouncer.scan.framework.api.dto\n\nimport com.getbouncer.scan.framework.RepeatingTaskStats\nimport com.getbouncer.scan.framework.Stats\nimport com.getbouncer.scan.framework.TaskStats\nimport com.getbouncer.scan.framework.ml.ModelLoadDetails\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class StatsPayload(\n    @SerialName(\"instance_id\") val instanceId: String,\n    @SerialName(\"scan_id\") val scanId: String?,\n    @SerialName(\"payload_version\") val payloadVersion: Int = 2,\n    @SerialName(\"device\") val device: ClientDevice,\n    @SerialName(\"app\") val app: AppInfo,\n    @SerialName(\"scan_stats\") val scanStats: ScanStatistics,\n    @SerialName(\"model_versions\") val modelVersions: List<ModelVersion>,\n)\n\n@Serializable\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class ScanStatistics(\n    @SerialName(\"tasks\") val tasks: Map<String, List<TaskStatistics>>,\n    @SerialName(\"repeating_tasks\") val repeatingTasks: Map<String, List<RepeatingTaskStatistics>>\n) {\n    companion object {\n        @JvmStatic\n        fun fromStats() = ScanStatistics(\n            tasks = Stats.getTasks().mapValues { entry ->\n                entry.value.map { TaskStatistics.fromTaskStats(it) }\n            },\n            repeatingTasks = Stats.getRepeatingTasks().mapValues { repeatingTasks ->\n                repeatingTasks.value.map { resultMap ->\n                    RepeatingTaskStatistics.fromRepeatingTaskStats(\n                        result = resultMap.key,\n                        repeatingTaskStats = resultMap.value,\n                    )\n                }\n            }\n        )\n    }\n}\n\n@Serializable\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class ModelVersion(\n    @SerialName(\"name\") val name: String,\n    @SerialName(\"version\") val version: String,\n    @SerialName(\"framework_version\") val frameworkVersion: Int,\n    @SerialName(\"loaded_successfully\") val loadedSuccessfully: Boolean\n) {\n    companion object {\n        fun fromModelLoadDetails(details: ModelLoadDetails) = ModelVersion(\n            name = details.modelClass,\n            version = details.modelVersion,\n            frameworkVersion = details.modelFrameworkVersion,\n            loadedSuccessfully = details.success\n        )\n    }\n}\n\n@Serializable\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class TaskStatistics(\n    @SerialName(\"started_at_ms\") val startedAtMs: Long,\n    @SerialName(\"duration_ms\") val durationMs: Long,\n    @SerialName(\"result\") val result: String?\n) {\n    companion object {\n        @JvmStatic\n        fun fromTaskStats(taskStats: TaskStats) = TaskStatistics(\n            startedAtMs = taskStats.started.toMillisecondsSinceEpoch(),\n            durationMs = taskStats.duration.inMilliseconds.toLong(),\n            result = taskStats.result\n        )\n    }\n}\n\n@Serializable\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class RepeatingTaskStatistics(\n    @SerialName(\"result\") val result: String,\n    @SerialName(\"executions\") val executions: Int,\n    @SerialName(\"start_time_ms\") val startTimeMs: Long,\n    @SerialName(\"total_duration_ms\") val totalDurationMs: Long,\n    @SerialName(\"total_cpu_duration_ms\") val totalCpuDurationMs: Long,\n    @SerialName(\"average_duration_ms\") val averageDurationMs: Long,\n    @SerialName(\"minimum_duration_ms\") val minimumDurationMs: Long,\n    @SerialName(\"maximum_duration_ms\") val maximumDurationMs: Long,\n) {\n    companion object {\n        @JvmStatic\n        fun fromRepeatingTaskStats(result: String, repeatingTaskStats: RepeatingTaskStats) = RepeatingTaskStatistics(\n            result = result,\n            executions = repeatingTaskStats.executions,\n            startTimeMs = repeatingTaskStats.startedAt.toMillisecondsSinceEpoch(),\n            totalDurationMs = repeatingTaskStats.totalDuration.inMilliseconds.toLong(),\n            totalCpuDurationMs = repeatingTaskStats.totalCpuDuration.inMilliseconds.toLong(),\n            averageDurationMs = repeatingTaskStats.averageDuration().inMilliseconds.toLong(),\n            minimumDurationMs = repeatingTaskStats.minimumDuration.inMilliseconds.toLong(),\n            maximumDurationMs = repeatingTaskStats.maximumDuration.inMilliseconds.toLong(),\n        )\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/ModelDetails.kt",
    "content": "package com.getbouncer.scan.framework.api.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class ModelDetailsRequest(\n    @SerialName(\"platform\") val platform: String,\n    @SerialName(\"model_class\") val modelClass: String,\n    @SerialName(\"model_framework_version\") val modelFrameworkVersion: Int,\n    @SerialName(\"cached_model_hash\") val cachedModelHash: String?,\n    @SerialName(\"cached_model_hash_algorithm\") val cachedModelHashAlgorithm: String?,\n    @SerialName(\"beta_opt_in\") val betaOptIn: Boolean?,\n)\n\n@Serializable\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class ModelDetailsResponse(\n    @SerialName(\"model_url\") val url: String?,\n    @SerialName(\"model_version\") val modelVersion: String,\n    @SerialName(\"model_hash\") val hash: String,\n    @SerialName(\"model_hash_algorithm\") val hashAlgorithm: String,\n    @SerialName(\"query_again_after_ms\") val queryAgainAfterMs: Long? = 0,\n)\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/ModelSignedUrlResponse.kt",
    "content": "package com.getbouncer.scan.framework.api.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class ModelSignedUrlResponse(\n    @SerialName(\"model_url\") val modelUrl: String\n)\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/ValidateApiKeyResponse.kt",
    "content": "package com.getbouncer.scan.framework.api.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class ValidateApiKeyResponse(\n    @SerialName(\"is_valid_api_key\") val isApiKeyValid: Boolean,\n    @SerialName(\"invalid_key_reason\") val keyInvalidReason: String?\n)\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/exception/ImageTypeNotSupportedException.kt",
    "content": "package com.getbouncer.scan.framework.exception\n\nimport java.lang.Exception\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nclass ImageTypeNotSupportedException(val imageType: Int) : Exception()\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/exception/InvalidBouncerApiKeyException.kt",
    "content": "package com.getbouncer.scan.framework.exception\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nobject InvalidBouncerApiKeyException : Exception()\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/image/BitmapExtensions.kt",
    "content": "package com.getbouncer.scan.framework.image\n\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport android.graphics.Matrix\nimport android.graphics.Rect\nimport android.util.Size\nimport androidx.annotation.CheckResult\nimport com.getbouncer.scan.framework.util.centerOn\nimport com.getbouncer.scan.framework.util.intersectionWith\nimport com.getbouncer.scan.framework.util.move\nimport com.getbouncer.scan.framework.util.resizeRegion\nimport com.getbouncer.scan.framework.util.size\nimport com.getbouncer.scan.framework.util.toRect\nimport kotlin.math.max\nimport kotlin.math.min\n\n/**\n * Crop a [Bitmap] to a given [Rect]. The crop must have a positive area and must be contained within the bounds of the\n * source [Bitmap].\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Bitmap.crop(crop: Rect): Bitmap {\n    require(crop.left < crop.right && crop.top < crop.bottom) { \"Cannot use negative crop\" }\n    require(crop.left >= 0 && crop.top >= 0 && crop.bottom <= this.height && crop.right <= this.width) {\n        \"Crop is larger than source image\"\n    }\n    return Bitmap.createBitmap(this, crop.left, crop.top, crop.width(), crop.height())\n}\n\n/**\n * Rotate a [Bitmap] by the given [rotationDegrees].\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Bitmap.rotate(rotationDegrees: Float): Bitmap = if (rotationDegrees != 0F) {\n    val matrix = Matrix()\n    matrix.postRotate(rotationDegrees)\n    Bitmap.createBitmap(this, 0, 0, this.width, this.height, matrix, true)\n} else {\n    this\n}\n\n/**\n * Scale a [Bitmap] by a given [percentage].\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Bitmap.scale(percentage: Float, filter: Boolean = false): Bitmap = if (percentage == 1F) {\n    this\n} else {\n    Bitmap.createScaledBitmap(\n        this,\n        (width * percentage).toInt(),\n        (height * percentage).toInt(),\n        filter\n    )\n}\n\n/**\n * Get the size of a [Bitmap].\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Bitmap.size() = Size(this.width, this.height)\n\n/**\n * Scale the [Bitmap] to circumscribe the given [Size], then crop the excess.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Bitmap.scaleAndCrop(size: Size, filter: Boolean = false): Bitmap =\n    if (size.width == width && size.height == height) {\n        this\n    } else {\n        val scaleFactor = max(size.width.toFloat() / this.width, size.height.toFloat() / this.height)\n        val scaled = this.scale(scaleFactor, filter)\n        scaled.crop(size.centerOn(scaled.size().toRect()))\n    }\n\n/**\n * Crops and image using originalImageRect and places it on finalImageRect, which is filled with\n * gray for the best results\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Bitmap.cropWithFill(cropRegion: Rect): Bitmap {\n    val intersectionRegion = this.size().toRect().intersectionWith(cropRegion)\n    val result = Bitmap.createBitmap(cropRegion.width(), cropRegion.height(), this.config)\n    val canvas = Canvas(result)\n\n    canvas.drawColor(Color.GRAY)\n\n    val croppedImage = this.crop(intersectionRegion)\n\n    canvas.drawBitmap(\n        croppedImage,\n        croppedImage.size().toRect(),\n        intersectionRegion.move(-cropRegion.left, -cropRegion.top),\n        null\n    )\n\n    return result\n}\n\n/**\n * Fragments the [Bitmap] into multiple segments and places them in new segments.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Bitmap.rearrangeBySegments(\n    segmentMap: Map<Rect, Rect>\n): Bitmap {\n    if (segmentMap.isEmpty()) {\n        return Bitmap.createBitmap(0, 0, this.config)\n    }\n    val newImageDimensions = segmentMap.values.reduce { a, b ->\n        Rect(\n            min(a.left, b.left),\n            min(a.top, b.top),\n            max(a.right, b.right),\n            max(a.bottom, b.bottom)\n        )\n    }\n    val newImageSize = newImageDimensions.size()\n    val result = Bitmap.createBitmap(newImageSize.width, newImageSize.height, this.config)\n    val canvas = Canvas(result)\n\n    // This should be using segmentMap.forEach, but doing so seems to require API 24. It's unclear why this won't use\n    // the kotlin.collections version of `forEach`, but it's not during compile.\n    for (it in segmentMap) {\n        val from = it.key\n        val to = it.value.move(-newImageDimensions.left, -newImageDimensions.top)\n\n        val segment = this.crop(from).scale(to.size())\n        canvas.drawBitmap(\n            segment,\n            to.left.toFloat(),\n            to.top.toFloat(),\n            null\n        )\n    }\n\n    return result\n}\n\n/**\n * Selects a region from the source [Bitmap], resizing that to a new region, and transforms the remainder of the\n * [Bitmap] into a border. See [resizeRegion] and [rearrangeBySegments].\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Bitmap.zoom(\n    originalRegion: Rect,\n    newRegion: Rect,\n    newImageSize: Size\n): Bitmap {\n    // Produces a map of rects to rects which are used to map segments of the old image onto the new one\n    val regionMap = this.size().resizeRegion(originalRegion, newRegion, newImageSize)\n    // construct the bitmap from the region map\n    return this.rearrangeBySegments(regionMap)\n}\n\nfun Bitmap.scale(size: Size, filter: Boolean = false): Bitmap =\n    if (size.width == width && size.height == height) {\n        this\n    } else {\n        Bitmap.createScaledBitmap(this, size.width, size.height, filter)\n    }\n\n/**\n * Convert a [Bitmap] to an [MLImage] for use in ML models.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Bitmap.toMLImage(mean: Float = 0F, std: Float = 255F) = MLImage(this, mean, std)\n\n/**\n * Convert a [Bitmap] to an [MLImage] for use in ML models.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Bitmap.toMLImage(mean: ImageTransformValues, std: ImageTransformValues) = MLImage(this, mean, std)\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/image/ImageExtensions.kt",
    "content": "package com.getbouncer.scan.framework.image\n\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.graphics.ImageFormat\nimport android.graphics.Rect\nimport android.media.Image\nimport android.renderscript.RenderScript\nimport androidx.annotation.CheckResult\nimport com.getbouncer.scan.framework.exception.ImageTypeNotSupportedException\n\n/**\n * Determine if this application supports an image format.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Image.isSupportedFormat() = isSupportedFormat(this.format)\n\n/**\n * Determine if this application supports an image format.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun isSupportedFormat(imageFormat: Int) = when (imageFormat) {\n    ImageFormat.YUV_420_888, ImageFormat.JPEG -> true\n    ImageFormat.NV21 -> false // this fails on devices with android API 21.\n    else -> false\n}\n\n/**\n * Convert an image to a bitmap for processing. This will throw an [ImageTypeNotSupportedException]\n * if the image type is not supported (see [isSupportedFormat]).\n */\n@CheckResult\n@Throws(ImageTypeNotSupportedException::class)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Image.toBitmap(\n    renderScript: RenderScript,\n    crop: Rect = Rect(\n        0,\n        0,\n        this.width,\n        this.height\n    ),\n): Bitmap = when (this.format) {\n    ImageFormat.NV21 -> NV21Image(this).crop(crop).toBitmap(renderScript)\n    ImageFormat.YUV_420_888 -> NV21Image(this).crop(crop).toBitmap(renderScript)\n    ImageFormat.JPEG -> jpegToBitmap().crop(crop)\n    else -> throw ImageTypeNotSupportedException(this.format)\n}\n\n@CheckResult\nprivate fun Image.jpegToBitmap(): Bitmap {\n    require(format == ImageFormat.JPEG) { \"Image is not in JPEG format\" }\n\n    val imageBuffer = planes[0].buffer\n    val imageBytes = ByteArray(imageBuffer.remaining())\n    imageBuffer.get(imageBytes)\n    return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/image/MLImage.kt",
    "content": "package com.getbouncer.scan.framework.image\n\nimport android.graphics.Bitmap\nimport java.nio.ByteBuffer\nimport java.nio.ByteOrder\nimport java.nio.IntBuffer\nimport kotlin.math.roundToInt\n\nprivate const val DIM_PIXEL_SIZE = 3\nprivate const val NUM_BYTES_PER_CHANNEL = 4 // Float.size / Byte.size\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class ImageTransformValues(val red: Float, val green: Float, val blue: Float)\n\n/**\n * An image in the required ML input format (array of floats, 3 floats per pixel in R, G, B format).\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nclass MLImage(val width: Int, val height: Int, private val imageData: ByteBuffer) {\n\n    constructor(bitmap: Bitmap, mean: Float = 0F, std: Float = 255F) : this(\n        bitmap,\n        ImageTransformValues(mean, mean, mean),\n        ImageTransformValues(std, std, std),\n    )\n\n    constructor(bitmap: Bitmap, mean: ImageTransformValues, std: ImageTransformValues) : this(\n        bitmap.width,\n        bitmap.height,\n        IntArray(bitmap.width * bitmap.height)\n            .also { bitmap.getPixels(it, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) }\n            .let {\n                val rgbFloat =\n                    ByteBuffer.allocateDirect(bitmap.width * bitmap.height * DIM_PIXEL_SIZE * NUM_BYTES_PER_CHANNEL)\n                rgbFloat.order(ByteOrder.nativeOrder())\n\n                it.forEach {\n                    // ignore the alpha value ((it shr 24 and 0xFF) - mean.alpha) / std.alpha)\n                    rgbFloat.putFloat(((it shr 16 and 0xFF) - mean.red) / std.red)\n                    rgbFloat.putFloat(((it shr 8 and 0xFF) - mean.green) / std.green)\n                    rgbFloat.putFloat(((it and 0xFF) - mean.blue) / std.blue)\n                }\n\n                rgbFloat\n            }\n    )\n\n    /**\n     * Convert an [MLImage] to a [Bitmap]. This is primarily used in testing.\n     */\n    fun toBitmap(mean: Float = 0F, std: Float = 255F) = toBitmap(\n        ImageTransformValues(mean, mean, mean),\n        ImageTransformValues(std, std, std),\n    )\n\n    /**\n     * Convert an [MLImage] to a [Bitmap]. This is primarily used in testing.\n     */\n    fun toBitmap(mean: ImageTransformValues, std: ImageTransformValues): Bitmap {\n        imageData.rewind()\n        check(imageData.limit() == width * height) { \"ByteBuffer limit does not match expected size\" }\n        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)\n        val rgba = IntBuffer.allocate(width * height)\n        while (this.imageData.hasRemaining()) {\n            rgba.put(\n                (0xFF shl 24) + // set 0xFF for the alpha value\n                    (((this.imageData.float * std.red) + mean.red).roundToInt() shl 16) +\n                    (((this.imageData.float * std.green) + mean.green).roundToInt() shl 8) +\n                    (((this.imageData.float * std.blue) + mean.blue).roundToInt())\n            )\n        }\n        rgba.rewind()\n        bitmap.copyPixelsFromBuffer(rgba)\n        return bitmap\n    }\n\n    /**\n     * Get the RBG direct [ByteBuffer] for use in ML models.\n     */\n    fun getData(): ByteBuffer = imageData.rewind() as ByteBuffer\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/image/NV21Image.kt",
    "content": "package com.getbouncer.scan.framework.image\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.graphics.ImageFormat\nimport android.graphics.Rect\nimport android.graphics.YuvImage\nimport android.media.Image\nimport android.renderscript.Allocation\nimport android.renderscript.Element\nimport android.renderscript.RenderScript\nimport android.renderscript.ScriptIntrinsicYuvToRGB\nimport android.renderscript.Type\nimport android.util.Log\nimport android.util.Size\nimport androidx.annotation.CheckResult\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.exception.ImageTypeNotSupportedException\nimport com.getbouncer.scan.framework.util.cacheFirstResult\nimport com.getbouncer.scan.framework.util.mapArray\nimport com.getbouncer.scan.framework.util.mapToIntArray\nimport com.getbouncer.scan.framework.util.toByteArray\nimport java.io.ByteArrayOutputStream\nimport java.io.IOException\nimport java.nio.ByteBuffer\nimport java.nio.ReadOnlyBufferException\nimport kotlin.experimental.inv\n\n/**\n * Get the RenderScript instance.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval getRenderScript = cacheFirstResult { context: Context -> RenderScript.create(context) }\n\n/**\n * An image made of data in the NV21 format.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nclass NV21Image(val width: Int, val height: Int, val nv21Data: ByteArray) {\n\n    @Throws(ImageTypeNotSupportedException::class)\n    constructor(image: Image) : this(\n        image.width,\n        image.height,\n        when (image.format) {\n            ImageFormat.NV21 -> image.planes[0].buffer.toByteArray()\n            ImageFormat.YUV_420_888 -> image.yuvToNV21Bytes()\n            else -> throw ImageTypeNotSupportedException(image.format)\n        }\n    )\n\n    @Throws(ImageTypeNotSupportedException::class)\n    constructor(yuvImage: YuvImage) : this(\n        yuvImage.width,\n        yuvImage.height,\n        when (yuvImage.yuvFormat) {\n            ImageFormat.NV21 -> yuvImage.yuvData\n            else -> throw ImageTypeNotSupportedException(yuvImage.yuvFormat)\n        }\n    )\n\n    /**\n     * The size of the [NV21Image].\n     */\n    val size = Size(width, height)\n\n    /**\n     * Crop a region of the [NV21Image].\n     *\n     * https://www.programmersought.com/article/75461140907/\n     */\n    fun crop(rect: Rect) = crop(rect.left, rect.top, rect.right, rect.bottom)\n\n    /**\n     * Crop a region of the [NV21Image].\n     *\n     * https://www.programmersought.com/article/75461140907/\n     */\n    fun crop(left: Int, top: Int, right: Int, bottom: Int): NV21Image {\n        if (left > width || top > height) {\n            return NV21Image(0, 0, ByteArray(0))\n        }\n\n        if (left == 0 && top == 0 && right == width && bottom == height) {\n            return this\n        }\n\n        // Take the couple\n        val x = left * 2 / 2\n        val y = top * 2 / 2\n        val w = (right - left) * 2 / 2\n        val h = (bottom - top) * 2 / 2\n\n        val yUnit = w * h\n        val uv = yUnit / 2\n        val nData = ByteArray(yUnit + uv)\n        val uvIndexDst = w * h - y / 2 * w\n        val uvIndexSrc = width * height + x\n        var srcPos0 = y * width\n        var destPos0 = 0\n        var uvSrcPos0 = uvIndexSrc\n        var uvDestPos0 = uvIndexDst\n        for (i in y until y + h) {\n            System.arraycopy(nv21Data, srcPos0 + x, nData, destPos0, w) // y memory block copy\n            srcPos0 += width\n            destPos0 += w\n            if ((i and 1) == 0) {\n                System.arraycopy(nv21Data, uvSrcPos0, nData, uvDestPos0, w) // uv memory block copy\n                uvSrcPos0 += width\n                uvDestPos0 += w\n            }\n        }\n\n        return NV21Image(w, h, nData)\n    }\n\n    /**\n     * Rotate the NV21 image an increment of 90 degrees.\n     */\n    fun rotate(rotationDegrees: Int): NV21Image {\n        require(rotationDegrees % 90 == 0) { \"Can only rotate increments of 90 degrees\" }\n        val rotation = if (rotationDegrees % 360 < 0) rotationDegrees % 360 + 360 else rotationDegrees % 360\n        if (rotation == 0) return this\n        val output = ByteArray(nv21Data.size)\n        val frameSize = width * height\n        val swap = rotation % 180 != 0\n        val xFlip = rotation % 270 != 0\n        val yFlip = rotation >= 180\n        for (j in 0 until height) {\n            for (i in 0 until width) {\n                val yIn = j * width + i\n                val uIn = frameSize + (j shr 1) * width + (i and 1.inv())\n                val vIn = uIn + 1\n                val wOut = if (swap) height else width\n                val hOut = if (swap) width else height\n                val iSwapped = if (swap) j else i\n                val jSwapped = if (swap) i else j\n                val iOut = if (xFlip) wOut - iSwapped - 1 else iSwapped\n                val jOut = if (yFlip) hOut - jSwapped - 1 else jSwapped\n                val yOut = jOut * wOut + iOut\n                val uOut = frameSize + (jOut shr 1) * wOut + (iOut and 1.inv())\n                val vOut = uOut + 1\n                output[yOut] = (0xff and nv21Data[yIn].toInt()).toByte()\n                output[uOut] = (0xff and nv21Data[uIn].toInt()).toByte()\n                output[vOut] = (0xff and nv21Data[vIn].toInt()).toByte()\n            }\n        }\n\n        return if (rotation == 270 || rotation == 90) {\n            NV21Image(height, width, output)\n        } else {\n            NV21Image(width, height, output)\n        }\n    }\n\n//    fun scale(percent: Float) {\n//        TODO(\"Implement this\")\n//    }\n\n//    fun toRGBByteBuffer(mean: ImageTransformValues, std: ImageTransformValues) {\n//        TODO(\"Finish implementing this\")\n//        val startTime = System.currentTimeMillis()\n//        val frameSize = width * height\n//\n//        var yp = 0\n//\n//        val rgba = IntArray(width * height)\n//\n//        for (j in 0 until height) {\n//            var uvp = frameSize + (j shr 1) * width\n//            var u = 0\n//            var v = 0\n//            for (i in 0 until width) {\n//                var y = (0xff and data[yp].toInt()) - 16\n//                if (y < 0) y = 0\n//                if (i and 1 == 0) {\n//                    v = (0xff and data[uvp++].toInt()) - 128\n//                    u = (0xff and data[uvp++].toInt()) - 128\n//                }\n//                val y1192 = 1192 * y\n//                var r = y1192 + 1634 * v\n//                var g = y1192 - 833 * v - 400 * u\n//                var b = y1192 + 2066 * u\n//                if (r < 0) r = 0 else if (r > 262143) r = 262143\n//                if (g < 0) g = 0 else if (g > 262143) g = 262143\n//                if (b < 0) b = 0 else if (b > 262143) b = 262143\n//\n//                // rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) &\n//                // 0xff00) | ((b >> 10) & 0xff);\n//                // rgba, divide 2^10 ( >> 10)\n//                rgba[yp] = (r shl 14 and -0x1000000 or (g shl 6 and 0xff0000)\n//                    or (b shr 2 or 0xff00))\n//                yp++\n//            }\n//        }\n//\n//        val rgbFloat =\n//            ByteBuffer.allocateDirect(this.width * this.height * DIM_PIXEL_SIZE * NUM_BYTES_PER_CHANNEL)\n//        rgbFloat.order(ByteOrder.nativeOrder())\n//\n//        rgba.forEach {\n//            rgbFloat.putFloat(((it shr 24 and 0xFF) - mean.red) / std.red)\n//            rgbFloat.putFloat(((it shr 16 and 0xFF) - mean.green) / std.green)\n//            rgbFloat.putFloat(((it shr 8 and 0xFF) - mean.blue) / std.blue)\n//            // ignore the alpha value ((it and 0xFF) - mean.alpha) / std.alpha)\n//        }\n//\n//        rgbFloat.rewind()\n//        Log.d(Config.logTag, \"Bitmap to RGB Byte buffer conversion took: ${System.currentTimeMillis() - startTime} ms\")\n//        return rgbFloat\n//    }\n\n    /**\n     * Convert to a [YuvImage].\n     */\n    @CheckResult\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    fun toYuvImage() = YuvImage(\n        nv21Data,\n        ImageFormat.NV21,\n        width,\n        height,\n        null\n    )\n\n    /**\n     * https://github.com/silvaren/easyrs/blob/c8eed0f0b713bbb1eb375aca23d615677e8adb3c/easyrs/src/main/java/io/github/silvaren/easyrs/tools/YuvToRgb.java\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    fun toBitmap(renderScript: RenderScript): Bitmap {\n        val yuvTypeBuilder: Type.Builder = Type.Builder(renderScript, Element.U8(renderScript)).setX(nv21Data.size)\n        val yuvType: Type = yuvTypeBuilder.create()\n        val yuvAllocation = Allocation.createTyped(renderScript, yuvType, Allocation.USAGE_SCRIPT)\n        yuvAllocation.copyFrom(nv21Data)\n\n        val rgbTypeBuilder: Type.Builder = Type.Builder(renderScript, Element.RGBA_8888(renderScript))\n        rgbTypeBuilder.setX(width)\n        rgbTypeBuilder.setY(height)\n        val rgbAllocation = Allocation.createTyped(renderScript, rgbTypeBuilder.create())\n\n        val yuvToRgbScript = ScriptIntrinsicYuvToRGB.create(renderScript, Element.RGBA_8888(renderScript))\n        yuvToRgbScript.setInput(yuvAllocation)\n        yuvToRgbScript.forEach(rgbAllocation)\n\n        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)\n        rgbAllocation.copyTo(bitmap)\n\n        // remove allocated objects\n        yuvType.destroy()\n        yuvAllocation.destroy()\n        rgbAllocation.destroy()\n        yuvToRgbScript.destroy()\n\n        return bitmap\n    }\n}\n\n/**\n * Convert YUV420_888 image into NV21\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nprivate fun Image.yuvToNV21Bytes() = yuvPlanesToNV21Fast(\n    width = width,\n    height = height,\n    planeBuffers = planes.mapArray { it.buffer },\n    rowStrides = planes.mapToIntArray { it.rowStride },\n    pixelStrides = planes.mapToIntArray { it.pixelStride },\n)\n\n/**\n * https://stackoverflow.com/questions/32276522/convert-nv21-byte-array-into-bitmap-readable-format\n *\n * https://stackoverflow.com/questions/41773621/camera2-output-to-bitmap\n *\n * On Revvl2, average performance is ~27ms\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun yuvPlanesToNV21Compat(\n    width: Int,\n    height: Int,\n    planeBuffers: Array<ByteBuffer>,\n    rowStrides: IntArray,\n    pixelStrides: IntArray,\n    format: Int,\n    crop: Rect = Rect(0, 0, width, height),\n): ByteArray {\n    val cropWidth = crop.width()\n    val cropHeight = crop.height()\n    val nv21Bytes = ByteArray(cropWidth * cropHeight * ImageFormat.getBitsPerPixel(format) / 8)\n    val rowData = ByteArray(rowStrides[0])\n\n    var channelOffset = 0\n    var outputStride = 1\n\n    for (i in planeBuffers.indices) {\n        when (i) {\n            0 -> {\n                channelOffset = 0\n                outputStride = 1\n            }\n            1 -> {\n                channelOffset = cropWidth * cropHeight + 1\n                outputStride = 2\n            }\n            2 -> {\n                channelOffset = cropWidth * cropHeight\n                outputStride = 2\n            }\n        }\n\n        val buffer = planeBuffers[i]\n        val rowStride = rowStrides[i]\n        val pixelStride = pixelStrides[i]\n        val shift = if (i == 0) 0 else 1\n        val w = cropWidth shr shift\n        val h = cropHeight shr shift\n\n        buffer.position(rowStride * (crop.top shr shift) + pixelStride * (crop.left shr shift))\n\n        for (row in 0 until h) {\n            var length: Int\n\n            if (pixelStride == 1 && outputStride == 1) {\n                length = w\n                buffer.get(nv21Bytes, channelOffset, length)\n                channelOffset += length\n            } else {\n                length = (w - 1) * pixelStride + 1\n                buffer.get(rowData, 0, length)\n                for (col in 0 until w) {\n                    nv21Bytes[channelOffset] = rowData[col * pixelStride]\n                    channelOffset += outputStride\n                }\n            }\n\n            if (row < h - 1) {\n                buffer.position(buffer.position() + rowStride - length)\n            }\n        }\n    }\n\n    return nv21Bytes\n}\n\n/**\n * https://stackoverflow.com/questions/44994510/how-to-convert-rotate-raw-nv21-array-image-android-media-image-from-front-ca\n *\n * On Revvl2, average performance is ~60ms\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun yuvPlanesToNV21Slow(planeBuffers: Array<ByteBuffer>): ByteArray {\n    val rez: ByteArray\n    val buffer0 = planeBuffers[0]\n    val buffer1 = planeBuffers[1]\n    val buffer2 = planeBuffers[2]\n\n    // actually here should be something like each second byte\n    // however I simply get the last byte of buffer 2 and the entire buffer 1\n    val buffer0Size = buffer0.remaining()\n    val buffer1Size = buffer1.remaining() // / 2 + 1;\n    val buffer2Size = 1 // buffer2.remaining(); // / 2 + 1;\n    val buffer0Byte = ByteArray(buffer0Size)\n    val buffer1Byte = ByteArray(buffer1Size)\n    val buffer2Byte = ByteArray(buffer2Size)\n    buffer0[buffer0Byte, 0, buffer0Size]\n    buffer1[buffer1Byte, 0, buffer1Size]\n    buffer2[buffer2Byte, buffer2Size - 1, buffer2Size]\n    val outputStream = ByteArrayOutputStream()\n    try {\n        // swap 1 and 2 as blue and red colors are swapped\n        outputStream.write(buffer0Byte)\n        outputStream.write(buffer2Byte)\n        outputStream.write(buffer1Byte)\n    } catch (e: IOException) {\n        Log.e(Config.logTag, \"Error converting image from YUV to NV21\")\n    }\n    rez = outputStream.toByteArray()\n\n    return rez\n}\n\n/**\n * Utility function for converting YUV planes into an NV21 byte array\n *\n * https://stackoverflow.com/questions/52726002/camera2-captured-picture-conversion-from-yuv-420-888-to-nv21/52740776#52740776\n *\n * On Revvl2, average performance is ~5ms\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun yuvPlanesToNV21Fast(\n    width: Int,\n    height: Int,\n    planeBuffers: Array<ByteBuffer>,\n    rowStrides: IntArray,\n    pixelStrides: IntArray,\n): ByteArray {\n    val ySize = width * height\n    val uvSize = width * height / 4\n    val nv21 = ByteArray(ySize + uvSize * 2)\n    val yBuffer = planeBuffers[0] // Y\n    val uBuffer = planeBuffers[1] // U\n    val vBuffer = planeBuffers[2] // V\n    var rowStride = rowStrides[0]\n    check(pixelStrides[0] == 1)\n    var pos = 0\n    if (rowStride == width) { // likely\n        yBuffer[nv21, 0, ySize]\n        pos += ySize\n    } else {\n        var yBufferPos = -rowStride.toLong() // not an actual position\n        while (pos < ySize) {\n            yBufferPos += rowStride.toLong()\n            yBuffer.position(yBufferPos.toInt())\n            yBuffer[nv21, pos, width]\n            pos += width\n        }\n    }\n    rowStride = rowStrides[2]\n    val pixelStride = pixelStrides[2]\n    check(rowStride == rowStrides[1])\n    check(pixelStride == pixelStrides[1])\n    if (pixelStride == 2 && rowStride == width && uBuffer[0] == vBuffer[1]) {\n        // maybe V an U planes overlap as per NV21, which means vBuffer[1] is alias of uBuffer[0]\n        val savePixel = vBuffer[1]\n        try {\n            vBuffer.put(1, savePixel.inv())\n            if (uBuffer[0] == savePixel.inv()) {\n                vBuffer.put(1, savePixel)\n                vBuffer.position(0)\n                uBuffer.position(0)\n                vBuffer[nv21, ySize, 1]\n                uBuffer[nv21, ySize + 1, uBuffer.remaining()]\n                return nv21 // shortcut\n            }\n        } catch (ex: ReadOnlyBufferException) {\n            // unfortunately, we cannot check if vBuffer and uBuffer overlap\n        }\n\n        // unfortunately, the check failed. We must save U and V pixel by pixel\n        vBuffer.put(1, savePixel)\n    }\n\n    // other optimizations could check if (pixelStride == 1) or (pixelStride == 2),\n    // but performance gain would be less significant\n    for (row in 0 until height / 2) {\n        for (col in 0 until width / 2) {\n            val vuPos = col * pixelStride + row * rowStride\n            nv21[pos++] = vBuffer[vuPos]\n            nv21[pos++] = uBuffer[vuPos]\n        }\n    }\n\n    return nv21\n}\n\n/**\n * https://stackoverflow.com/questions/33542708/camera2-api-convert-yuv420-to-rgb-green-out\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun yuvPlanesToBitmap(\n    width: Int,\n    height: Int,\n    planeBuffers: Array<ByteBuffer>,\n): Bitmap {\n    val bitmap = ByteArrayOutputStream()\n    YuvImage(planeBuffers.toList().toByteArray(), ImageFormat.NV21, width, height, null).compressToJpeg(Rect(0, 0, width, height), 95, bitmap)\n    return BitmapFactory.decodeByteArray(bitmap.toByteArray(), 0, bitmap.size())\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/image/YuvImageExtensions.kt",
    "content": "package com.getbouncer.scan.framework.image\n\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.graphics.Rect\nimport android.graphics.YuvImage\nimport androidx.annotation.CheckResult\nimport java.io.ByteArrayOutputStream\n\n/**\n * Convert a [YuvImage] to a [Bitmap]. This is not an efficient method since it uses an intermediate JPEG compression\n * and should be avoided if possible.\n */\n@CheckResult\n@Deprecated(\"This method is inefficient and should be avoided if possible\")\nfun YuvImage.toBitmap(\n    crop: Rect = Rect(\n        0,\n        0,\n        this.width,\n        this.height\n    ),\n    quality: Int = 75\n): Bitmap {\n    val out = ByteArrayOutputStream()\n    compressToJpeg(crop, quality, out)\n\n    val imageBytes = out.toByteArray()\n    return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/interop/BlockingAnalyzer.kt",
    "content": "package com.getbouncer.scan.framework.interop\n\nimport com.getbouncer.scan.framework.Analyzer\nimport com.getbouncer.scan.framework.AnalyzerFactory\nimport com.getbouncer.scan.framework.AnalyzerPool\nimport com.getbouncer.scan.framework.DEFAULT_ANALYZER_PARALLEL_COUNT\nimport kotlinx.coroutines.runBlocking\n\n/**\n * An implementation of an analyzer that does not use suspending functions. This allows interoperability with java.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nabstract class BlockingAnalyzer<Input, State, Output> : Analyzer<Input, State, Output> {\n    override suspend fun analyze(data: Input, state: State): Output = analyzeBlocking(data, state)\n\n    abstract fun analyzeBlocking(data: Input, state: State): Output\n}\n\n/**\n * An implementation of an analyzer factory that does not use suspending functions. This allows interoperability with\n * java.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nabstract class BlockingAnalyzerFactory<DataFrame, State, Output, AnalyzerType : Analyzer<DataFrame, State, Output>> : AnalyzerFactory<DataFrame, State, Output, AnalyzerType> {\n    override suspend fun newInstance(): AnalyzerType? = newInstanceBlocking()\n\n    abstract fun newInstanceBlocking(): AnalyzerType?\n}\n\n/**\n * An implementation of an analyzer pool factory that does not use suspending functions. This allows interoperability\n * with java.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nclass BlockingAnalyzerPoolFactory<DataFrame, State, Output> @JvmOverloads constructor(\n    private val analyzerFactory: AnalyzerFactory<DataFrame, State, Output, out Analyzer<DataFrame, State, Output>>,\n    private val desiredAnalyzerCount: Int = DEFAULT_ANALYZER_PARALLEL_COUNT\n) {\n    fun buildAnalyzerPool() = AnalyzerPool(\n        desiredAnalyzerCount = desiredAnalyzerCount,\n        analyzers = (0 until desiredAnalyzerCount).mapNotNull {\n            runBlocking { analyzerFactory.newInstance() }\n        }\n    )\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/interop/BlockingResult.kt",
    "content": "package com.getbouncer.scan.framework.interop\n\nimport com.getbouncer.scan.framework.AggregateResultListener\nimport com.getbouncer.scan.framework.ResultAggregator\nimport com.getbouncer.scan.framework.ResultHandler\nimport com.getbouncer.scan.framework.StatefulResultHandler\nimport com.getbouncer.scan.framework.TerminatingResultHandler\n\n/**\n * An implementation of a result handler that does not use suspending functions. This allows interoperability with java.\n */\nabstract class BlockingResultHandler<Input, Output, Verdict> : ResultHandler<Input, Output, Verdict> {\n    override suspend fun onResult(result: Output, data: Input) = onResultBlocking(result, data)\n\n    abstract fun onResultBlocking(result: Output, data: Input): Verdict\n}\n\n/**\n * An implementation of a stateful result handler that does not use suspending functions. This allows interoperability\n * with java.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nabstract class BlockingStatefulResultHandler<Input, State, Output, Verdict>(\n    initialState: State\n) : StatefulResultHandler<Input, State, Output, Verdict>(initialState) {\n    override suspend fun onResult(result: Output, data: Input): Verdict = onResultBlocking(result, data)\n\n    abstract fun onResultBlocking(result: Output, data: Input): Verdict\n}\n\n/**\n * An implementation of a terminating result handler that does not use suspending functions. This allows\n * interoperability with java.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nabstract class BlockingTerminatingResultHandler<Input, State, Output>(\n    initialState: State\n) : TerminatingResultHandler<Input, State, Output>(initialState) {\n    override suspend fun onResult(result: Output, data: Input) = onResultBlocking(result, data)\n\n    override suspend fun onTerminatedEarly() = onTerminatedEarlyBlocking()\n\n    override suspend fun onAllDataProcessed() = onAllDataProcessedBlocking()\n\n    abstract fun onResultBlocking(result: Output, data: Input)\n\n    abstract fun onTerminatedEarlyBlocking()\n\n    abstract fun onAllDataProcessedBlocking()\n}\n\n/**\n * An implementation of a result listener that does not use suspending functions. This allows interoperability with\n * java.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nabstract class BlockingAggregateResultListener<InterimResult, FinalResult> :\n    AggregateResultListener<InterimResult, FinalResult> {\n    override suspend fun onInterimResult(result: InterimResult) = onInterimResultBlocking(result)\n\n    override suspend fun onResult(result: FinalResult) = onResultBlocking(result)\n\n    override suspend fun onReset() = onResetBlocking()\n\n    abstract fun onInterimResultBlocking(result: InterimResult)\n\n    abstract fun onResultBlocking(result: FinalResult)\n\n    abstract fun onResetBlocking()\n}\n\n/**\n * An implementation of a result aggregator that does not use suspending functions. This allows interoperability with\n * java.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nabstract class BlockingResultAggregator<DataFrame, State, AnalyzerResult, InterimResult, FinalResult>(\n    listener: AggregateResultListener<InterimResult, FinalResult>,\n    initialState: State\n) : ResultAggregator<DataFrame, State, AnalyzerResult, InterimResult, FinalResult>(listener, initialState) {\n    override suspend fun aggregateResult(frame: DataFrame, result: AnalyzerResult): Pair<InterimResult, FinalResult?> =\n        aggregateResultBlocking(frame, result)\n\n    abstract fun aggregateResultBlocking(frame: DataFrame, result: AnalyzerResult): Pair<InterimResult, FinalResult?>\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/interop/JavaContinuation.kt",
    "content": "@file:JvmName(\"Coroutine\")\npackage com.getbouncer.scan.framework.interop\n\nimport android.util.Log\nimport com.getbouncer.scan.framework.Config\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.runBlocking\nimport kotlin.coroutines.Continuation\nimport kotlin.coroutines.CoroutineContext\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.resumeWithException\n\n/**\n * A utility class for calling suspend functions from java. This allows listening to a suspend function with callbacks.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nabstract class JavaContinuation<in T> @JvmOverloads constructor(\n    runOn: CoroutineContext = Dispatchers.Default,\n    private val listenOn: CoroutineContext = Dispatchers.Main\n) : Continuation<T> {\n    override val context: CoroutineContext = runOn\n    abstract fun onComplete(value: T)\n    abstract fun onException(exception: Throwable)\n    override fun resumeWith(result: Result<T>) = result.fold(\n        onSuccess = {\n            runBlocking(listenOn) {\n                onComplete(it)\n            }\n        },\n        onFailure = {\n            runBlocking(listenOn) {\n                onException(it)\n            }\n        }\n    )\n}\n\n/**\n * An empty continuation for ignoring results.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nclass EmptyJavaContinuation<in T> : JavaContinuation<T>() {\n    override fun onComplete(value: T) { }\n    override fun onException(exception: Throwable) {\n        Log.e(Config.logTag, \"Error in continuation\", exception)\n    }\n}\n\n/**\n * Resume a continuation with a value.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun <T> Continuation<T>.resumeJava(value: T) = this.resume(value)\n\n/**\n * Resume a continuation with an exception.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun <T> Continuation<T>.resumeWithExceptionJava(exception: Throwable) = this.resumeWithException(exception)\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/ml/ModelVersionTracker.kt",
    "content": "package com.getbouncer.scan.framework.ml\n\nprivate val MODEL_MAP = mutableMapOf<String, MutableSet<Triple<String, Int, Boolean>>>()\n\n/**\n * Details about a model loaded into memory.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class ModelLoadDetails(\n    val modelClass: String,\n    val modelVersion: String,\n    val modelFrameworkVersion: Int,\n    val success: Boolean\n)\n\n/**\n * When a ML model is loaded into memory, track the details of the model.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun trackModelLoaded(modelClass: String, modelVersion: String, modelFrameworkVersion: Int, success: Boolean) {\n    MODEL_MAP.getOrPut(modelClass) { mutableSetOf() }.add(Triple(modelVersion, modelFrameworkVersion, success))\n}\n\n/**\n * Get the full list of models that were loaded into memory during this session.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun getLoadedModelVersions(): List<ModelLoadDetails> = MODEL_MAP.flatMap { entry ->\n    entry.value.map {\n        ModelLoadDetails(\n            modelClass = entry.key,\n            modelVersion = it.first,\n            modelFrameworkVersion = it.second,\n            success = it.third\n        )\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/ml/NonMaximumSuppression.kt",
    "content": "package com.getbouncer.scan.framework.ml\n\nimport com.getbouncer.scan.framework.ml.ssd.RectForm\nimport com.getbouncer.scan.framework.ml.ssd.areaClamped\nimport com.getbouncer.scan.framework.ml.ssd.overlapWith\nimport java.util.ArrayList\n\n/**\n * In this project we implement HARD NMS and NOT Soft NMS. I highly recommend checkout SOFT NMS\n * implementation of Facebook Detectron Framework.\n *\n * See https://towardsdatascience.com/non-maximum-suppression-nms-93ce178e177c\n *\n * @param boxes: Detected boxes\n * @param probabilities: Probabilities of the given boxes\n * @param iouThreshold: intersection over union threshold.\n * @param limit: keep this number of results. If limit <= 0, keep all the results.\n *\n * @return pickedIndices: a list of indexes of the kept boxes\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun hardNonMaximumSuppression(\n    boxes: Array<FloatArray>,\n    probabilities: FloatArray,\n    iouThreshold: Float,\n    limit: Int?\n): ArrayList<Int> {\n    val indexArray = probabilities.indices.sortedByDescending { probabilities[it] }.take(200).toMutableList()\n    val pickedIndexes = ArrayList<Int>()\n\n    while (indexArray.isNotEmpty()) {\n        val current = indexArray.removeAt(0)\n        pickedIndexes.add(current)\n\n        if (pickedIndexes.size == limit) {\n            return pickedIndexes\n        }\n\n        val iterator = indexArray.iterator()\n        while (iterator.hasNext()) {\n            if (intersectionOverUnionOf(boxes[current], boxes[iterator.next()]) >= iouThreshold) {\n                iterator.remove()\n            }\n        }\n    }\n\n    return pickedIndexes\n}\n\n/**\n * Return intersection-over-union (Jaccard index) of boxes.\n *\n * Args:\n * boxes0 (N, 4): ground truth boxes.\n * boxes1 (N or 1, 4): predicted boxes.\n * eps: a small number to avoid 0 as denominator.\n * Returns: iou (N): IOU values\n */\nprivate fun intersectionOverUnionOf(currentBox: RectForm, nextBox: RectForm): Float {\n    val eps = 0.00001f\n    val overlapArea = nextBox.overlapWith(currentBox).areaClamped()\n    val nextArea = nextBox.areaClamped()\n    val currentArea = currentBox.areaClamped()\n    return overlapArea / (nextArea + currentArea - overlapArea + eps)\n}\n\n/**\n * Runs greedy NonMaxSuppression over the raw predictions. Greedy NMS looks for the local maximas\n * (\"peaks\") in the prediction confidences of the consecutive same predictions, keeps those,\n * and replaces the other values as the background class.\n *\n * Example: given the following [rawPredictions] and [confidence] pair\n *   [rawPredictions]: [LABEL0, LABEL0, LABEL0, LABEL1, LABEL1, LABEL1]\n *   [confidence]:     [0.1,    0.2,    0.4,    0.3,    0.5,   0.3]\n *   Output:           [BACKGROUND, BACKGROUND, LABEL0, BACKGROUND, LABEL, BACKGROUND]\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun <Input> greedyNonMaxSuppression(\n    rawPredictions: Array<Input>,\n    confidence: FloatArray,\n    backgroundClass: Input\n): Array<Input> {\n    val digits = rawPredictions.clone()\n\n    // greedy non max suppression\n    for (idx in 0 until digits.size - 1) {\n        if (digits[idx] != backgroundClass && digits[idx + 1] != backgroundClass) {\n            if (confidence[idx] < confidence[idx + 1]) {\n                digits[idx] = backgroundClass\n            } else {\n                digits[idx + 1] = backgroundClass\n            }\n        }\n    }\n    return digits\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/ml/TensorFlowLiteAnalyzer.kt",
    "content": "package com.getbouncer.scan.framework.ml\n\nimport android.content.Context\nimport android.util.Log\nimport com.getbouncer.scan.framework.Analyzer\nimport com.getbouncer.scan.framework.AnalyzerFactory\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.FetchedData\nimport com.getbouncer.scan.framework.Loader\nimport com.getbouncer.scan.framework.time.Timer\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport org.tensorflow.lite.Interpreter\nimport org.tensorflow.lite.nnapi.NnApiDelegate\nimport java.io.Closeable\nimport java.nio.ByteBuffer\n\n/**\n * A TensorFlowLite analyzer uses an [Interpreter] to analyze data.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nabstract class TensorFlowLiteAnalyzer<Input, MLInput, Output, MLOutput>(\n    private val tfInterpreter: Interpreter,\n    private val delegate: NnApiDelegate? = null,\n) : Analyzer<Input, Any, Output>, Closeable {\n\n    protected abstract suspend fun interpretMLOutput(data: Input, mlOutput: MLOutput): Output\n\n    protected abstract suspend fun transformData(data: Input): MLInput\n\n    protected abstract suspend fun executeInference(tfInterpreter: Interpreter, data: MLInput): MLOutput\n\n    private val loggingTimer by lazy {\n        Timer.newInstance(Config.logTag, this::class.java.simpleName)\n    }\n\n    override suspend fun analyze(data: Input, state: Any): Output {\n        val mlInput = loggingTimer.measureSuspend(\"transform\") {\n            transformData(data)\n        }\n\n        val mlOutput = loggingTimer.measureSuspend(\"infer\") {\n            executeInference(tfInterpreter, mlInput)\n        }\n\n        return loggingTimer.measureSuspend(\"interpret\") {\n            interpretMLOutput(data, mlOutput)\n        }\n    }\n\n    override fun close() {\n        tfInterpreter.close()\n        delegate?.close()\n    }\n}\n\n/**\n * A factory that creates tensorflow models as analyzers.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nabstract class TFLAnalyzerFactory<Input, Output, AnalyzerType : Analyzer<Input, Any, Output>>(\n    private val context: Context,\n    private val fetchedModel: FetchedData,\n) : AnalyzerFactory<Input, Any, Output, AnalyzerType> {\n    protected abstract val tfOptions: Interpreter.Options\n\n    private val loader by lazy { Loader(context) }\n\n    private val loadModelMutex = Mutex()\n\n    private var loadedModel: ByteBuffer? = null\n\n    protected suspend fun createInterpreter(): Interpreter? =\n        createInterpreter(fetchedModel)\n\n    private suspend fun createInterpreter(fetchedModel: FetchedData): Interpreter? = try {\n        loadModel(fetchedModel)?.let { Interpreter(it, tfOptions) }\n    } catch (t: Throwable) {\n        Log.e(Config.logTag, \"Error occurred while loading model ${fetchedModel.modelClass} version ${fetchedModel.modelVersion}\", t)\n        null\n    }.apply {\n        if (this == null) {\n            Log.w(Config.logTag, \"Unable to load model ${fetchedModel.modelClass} version ${fetchedModel.modelVersion}\")\n        }\n    }\n\n    private suspend fun loadModel(fetchedModel: FetchedData): ByteBuffer? = loadModelMutex.withLock {\n        loadedModel ?: run { loader.loadData(fetchedModel) }\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/ml/ssd/ClassifierScores.kt",
    "content": "package com.getbouncer.scan.framework.ml.ssd\n\nimport com.getbouncer.scan.framework.util.updateEach\nimport kotlin.math.exp\n\ntypealias ClassifierScores = FloatArray\n\n/**\n * Compute softmax for the given row. This will replace each row value with a value normalized by\n * the sum of all the values in the row.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun ClassifierScores.softMax() {\n    val rowSumExp = this.fold(0F) { acc, element -> acc + exp(element) }\n    this.updateEach { exp(it) / rowSumExp }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/ml/ssd/RectForm.kt",
    "content": "package com.getbouncer.scan.framework.ml.ssd\n\nimport android.graphics.RectF\nimport com.getbouncer.scan.framework.util.clamp\n\n/**\n * An array of four floats, which denote a rectangle of the following values:\n * [0] = left percent\n * [1] = top percent\n * [2] = right percent\n * [3] = bottom percent\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ntypealias RectForm = FloatArray\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nconst val RECT_FORM_SIZE = 4\n\n/**\n * Create a new [RectForm].\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun rectForm(left: Float, top: Float, right: Float, bottom: Float) =\n    RectForm(RECT_FORM_SIZE).apply {\n        setLeft(left)\n        setTop(top)\n        setRight(right)\n        setBottom(bottom)\n    }\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectForm.left() = this[0]\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectForm.top() = this[1]\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectForm.right() = this[2]\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectForm.bottom() = this[3]\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectForm.setLeft(left: Float) { this[0] = left }\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectForm.setTop(top: Float) { this[1] = top }\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectForm.setRight(right: Float) { this[2] = right }\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectForm.setBottom(bottom: Float) { this[3] = bottom }\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectForm.calcWidth() = right() - left()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectForm.calcHeight() = bottom() - top()\n\n/**\n * Convert this [RectForm] to a [RectF].\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectForm.toRectF() = RectF(left(), top(), right(), bottom())\n\n/**\n * Calculate the area of a rectangle while clamping the width and height between 0 and 1000.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectForm.areaClamped() = clamp(calcWidth(), 0F, 1000F) * clamp(calcHeight(), 0F, 1000F)\n\n/**\n * Create a rectangle of the overlap of this rectangle and another. Note that if the two rectangles\n * do not overlap, this can create a negative area rectangle.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectForm.overlapWith(other: RectForm) =\n    rectForm(\n        kotlin.math.max(this.left(), other.left()),\n        kotlin.math.max(this.top(), other.top()),\n        kotlin.math.min(this.right(), other.right()),\n        kotlin.math.min(this.bottom(), other.bottom())\n    )\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/ml/ssd/SizeAndCenter.kt",
    "content": "package com.getbouncer.scan.framework.ml.ssd\n\nimport com.getbouncer.scan.framework.util.clamp\nimport kotlin.math.exp\n\n/**\n * An array of four floats, which denote a rectangle of the following values:\n * [0] = centerX percent\n * [1] = centerY percent\n * [2] = width percent\n * [3] = height percent\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ntypealias SizeAndCenter = FloatArray\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nconst val SIZE_AND_CENTER_SIZE = 4\n\n/**\n * Create a new SizeAndCenter.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun sizeAndCenter(centerX: Float, centerY: Float, width: Float, height: Float) =\n    SizeAndCenter(SIZE_AND_CENTER_SIZE).apply {\n        setCenterX(centerX)\n        setCenterY(centerY)\n        setWidth(width)\n        setHeight(height)\n    }\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeAndCenter.centerX() = this[0]\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeAndCenter.centerY() = this[1]\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeAndCenter.width() = this[2]\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeAndCenter.height() = this[3]\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeAndCenter.setCenterX(centerX: Float) { this[0] = centerX }\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeAndCenter.setCenterY(centerY: Float) { this[1] = centerY }\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeAndCenter.setWidth(width: Float) { this[2] = width }\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeAndCenter.setHeight(height: Float) { this[3] = height }\n\n/**\n * Convert [SizeAndCenter] (centerX, centerY, w, h) to [RectForm] (left, top, right, bottom)\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeAndCenter.toRectForm() {\n    val left = centerX() - width() / 2\n    val top = centerY() - height() / 2\n    val right = centerX() + width() / 2\n    val bottom = centerY() + height() / 2\n\n    setLeft(left)\n    setTop(top)\n    setRight(right)\n    setBottom(bottom)\n}\n\n/**\n * Clamp all values in the array to the specified [minimum] and [maximum].\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeAndCenter.clampAll(minimum: Float, maximum: Float) {\n    setCenterX(clamp(centerX(), minimum, maximum))\n    setCenterY(clamp(centerY(), minimum, maximum))\n    setWidth(clamp(width(), minimum, maximum))\n    setHeight(clamp(height(), minimum, maximum))\n}\n\n/**\n * Convert a regressional location result of SSD into a []SizeAndCenter].\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeAndCenter.adjustLocation(\n    prior: SizeAndCenter,\n    centerVariance: Float,\n    sizeVariance: Float\n) {\n    setCenterX(centerX() * centerVariance * prior.width() + prior.centerX())\n    setCenterY(centerY() * centerVariance * prior.height() + prior.centerY())\n    setWidth(exp(width() * sizeVariance) * prior.width())\n    setHeight(exp(height() * sizeVariance) * prior.height())\n}\n\n/**\n * Convert regressional location results of SSD into [SizeAndCenter] arrays.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Array<SizeAndCenter>.adjustLocations(\n    priors: Array<SizeAndCenter>,\n    centerVariance: Float,\n    sizeVariance: Float\n) {\n    for (i in this.indices) {\n        this[i].adjustLocation(priors[i], centerVariance, sizeVariance)\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/time/Clock.kt",
    "content": "package com.getbouncer.scan.framework.time\n\nimport androidx.annotation.CheckResult\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nobject Clock {\n    @JvmStatic\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    fun markNow(): ClockMark = PreciseClockMark(System.nanoTime())\n}\n\n/**\n * Convert a milliseconds since epoch timestamp to a clock mark.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Long.asEpochMillisecondsClockMark(): ClockMark = AbsoluteClockMark(this)\n\n/**\n * A marked point in time.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsealed class ClockMark {\n    abstract fun elapsedSince(): Duration\n\n    abstract fun toMillisecondsSinceEpoch(): Long\n\n    abstract fun hasPassed(): Boolean\n\n    abstract fun isInFuture(): Boolean\n\n    abstract operator fun plus(duration: Duration): ClockMark\n\n    abstract operator fun minus(duration: Duration): ClockMark\n\n    abstract operator fun compareTo(other: ClockMark): Int\n}\n\n/**\n * A clock mark based on milliseconds since epoch. This is precise to the nearest millisecond.\n */\nprivate class AbsoluteClockMark(private val millisecondsSinceEpoch: Long) : ClockMark() {\n    override fun elapsedSince(): Duration = (System.currentTimeMillis() - millisecondsSinceEpoch).milliseconds\n\n    override fun toMillisecondsSinceEpoch(): Long = millisecondsSinceEpoch\n\n    override fun hasPassed(): Boolean = elapsedSince() > Duration.ZERO\n\n    override fun isInFuture(): Boolean = elapsedSince() < Duration.ZERO\n\n    override fun plus(duration: Duration): ClockMark =\n        AbsoluteClockMark(millisecondsSinceEpoch + duration.inMilliseconds.toLong())\n\n    override fun minus(duration: Duration): ClockMark =\n        AbsoluteClockMark(millisecondsSinceEpoch - duration.inMilliseconds.toLong())\n\n    override fun compareTo(other: ClockMark): Int =\n        millisecondsSinceEpoch.compareTo(other.toMillisecondsSinceEpoch())\n\n    override fun equals(other: Any?): Boolean =\n        this === other || when (other) {\n            is AbsoluteClockMark -> millisecondsSinceEpoch == other.millisecondsSinceEpoch\n            is ClockMark -> toMillisecondsSinceEpoch() == other.toMillisecondsSinceEpoch()\n            else -> false\n        }\n\n    override fun hashCode(): Int {\n        return millisecondsSinceEpoch.hashCode()\n    }\n\n    override fun toString(): String {\n        return \"AbsoluteClockMark(at $millisecondsSinceEpoch ms since epoch})\"\n    }\n}\n\n/**\n * A precise clock mark that is not bound to epoch seconds. This is precise to the nearest nanosecond.\n */\nprivate class PreciseClockMark(private val originMarkNanoseconds: Long) : ClockMark() {\n    override fun elapsedSince(): Duration = (System.nanoTime() - originMarkNanoseconds).nanoseconds\n\n    override fun toMillisecondsSinceEpoch(): Long = System.currentTimeMillis() - elapsedSince().inMilliseconds.toLong()\n\n    override fun hasPassed(): Boolean = elapsedSince() > Duration.ZERO\n\n    override fun isInFuture(): Boolean = elapsedSince() < Duration.ZERO\n\n    override fun plus(duration: Duration): ClockMark =\n        PreciseClockMark(originMarkNanoseconds + duration.inNanoseconds)\n\n    override fun minus(duration: Duration): ClockMark =\n        PreciseClockMark(originMarkNanoseconds + duration.inNanoseconds)\n\n    override fun compareTo(other: ClockMark): Int = elapsedSince().compareTo(other.elapsedSince())\n\n    override fun equals(other: Any?): Boolean =\n        this === other || when (other) {\n            is PreciseClockMark -> originMarkNanoseconds == other.originMarkNanoseconds\n            is ClockMark -> toMillisecondsSinceEpoch() == other.toMillisecondsSinceEpoch()\n            else -> false\n        }\n\n    override fun hashCode(): Int {\n        return originMarkNanoseconds.hashCode()\n    }\n\n    override fun toString(): String = elapsedSince().let {\n        if (it >= Duration.ZERO) {\n            \"PreciseClockMark($it ago)\"\n        } else {\n            \"PreciseClockMark(${-it} in the future)\"\n        }\n    }\n}\n\n/**\n * Measure the amount of time a process takes.\n *\n * TODO: use contracts when they are no longer experimental\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ninline fun <T> measureTime(block: () -> T): Pair<Duration, T> {\n    // contract { callsInPlace(block, EXACTLY_ONCE) }\n    val mark = Clock.markNow()\n    val result = block()\n    return mark.elapsedSince() to result\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/time/Coroutine.kt",
    "content": "package com.getbouncer.scan.framework.time\n\nimport kotlin.math.roundToLong\n\n/**\n * Allow delaying for a specified duration\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsuspend fun delay(duration: Duration) =\n    kotlinx.coroutines.delay(duration.inMilliseconds.roundToLong())\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/time/Duration.kt",
    "content": "package com.getbouncer.scan.framework.time\n\nimport kotlin.math.round\nimport kotlin.math.roundToLong\n\n/**\n * Round a number to a specified number of digits.\n */\nprivate fun Double.roundTo(numberOfDigits: Int): Double {\n    var multiplier = 1.0F\n    repeat(numberOfDigits) { multiplier *= 10 }\n    return round(this * multiplier) / multiplier\n}\n\n/**\n * Since kotlin time is still experimental, implement our own version for utility.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsealed class Duration : Comparable<Duration> {\n\n    companion object {\n        val ZERO: Duration = DurationNanoseconds(0)\n        val INFINITE: Duration = DurationInfinitePositive\n        val NEGATIVE_INFINITE: Duration = DurationInfiniteNegative\n    }\n\n    abstract val inYears: Double\n\n    abstract val inMonths: Double\n\n    abstract val inWeeks: Double\n\n    abstract val inDays: Double\n\n    abstract val inHours: Double\n\n    abstract val inMinutes: Double\n\n    abstract val inSeconds: Double\n\n    abstract val inMilliseconds: Double\n\n    abstract val inMicroseconds: Double\n\n    abstract val inNanoseconds: Long\n\n    override fun equals(other: Any?): Boolean =\n        if (other is Duration) inNanoseconds == other.inNanoseconds else false\n\n    override fun hashCode(): Int = inNanoseconds.toInt()\n\n    override fun toString(): String = when {\n        inYears > 1 -> \"${inYears.roundTo(2)} years\"\n        inMonths > 1 -> \"${inMonths.roundTo(2)} months\"\n        inWeeks > 1 -> \"${inWeeks.roundTo(2)} weeks\"\n        inDays > 1 -> \"${inDays.roundTo(2)} days\"\n        inHours > 1 -> \"${inHours.roundTo(2)} hours\"\n        inMinutes > 1 -> \"${inMinutes.roundTo(2)} minutes\"\n        inSeconds > 1 -> \"${inSeconds.roundTo(2)} seconds\"\n        inMilliseconds > 1 -> \"${inMilliseconds.roundTo(2)} milliseconds\"\n        inMicroseconds > 1 -> \"${inMicroseconds.roundTo(2)} microseconds\"\n        else -> \"$inNanoseconds nanoseconds\"\n    }\n\n    open operator fun plus(other: Duration): Duration = DurationNanoseconds(inNanoseconds + other.inNanoseconds)\n\n    open operator fun minus(other: Duration): Duration = DurationNanoseconds(inNanoseconds - other.inNanoseconds)\n\n    open operator fun times(multiplier: Int): Duration = DurationNanoseconds(inNanoseconds * multiplier)\n\n    open operator fun times(multiplier: Long): Duration = DurationNanoseconds(inNanoseconds * multiplier)\n\n    open operator fun times(multiplier: Float): Duration = DurationNanoseconds((inNanoseconds * multiplier.toDouble()).roundToLong())\n\n    open operator fun times(multiplier: Double): Duration = DurationNanoseconds((inNanoseconds * multiplier).roundToLong())\n\n    open operator fun div(denominator: Int): Duration = DurationNanoseconds(inNanoseconds / denominator)\n\n    open operator fun div(denominator: Long): Duration = DurationNanoseconds(inNanoseconds / denominator)\n\n    open operator fun div(denominator: Float): Duration = DurationNanoseconds((inNanoseconds / denominator.toDouble()).roundToLong())\n\n    open operator fun div(denominator: Double): Duration = DurationNanoseconds((inNanoseconds / denominator).roundToLong())\n\n    open operator fun unaryMinus(): Duration = DurationNanoseconds(-inNanoseconds)\n\n    override operator fun compareTo(other: Duration): Int = inNanoseconds.compareTo(other.inNanoseconds)\n}\n\nprivate abstract class DurationInfinite : Duration() {\n    override operator fun plus(other: Duration): Duration = this\n    override operator fun minus(other: Duration): Duration = this\n    override operator fun times(multiplier: Int): Duration = this\n    override operator fun times(multiplier: Long): Duration = this\n    override operator fun times(multiplier: Float): Duration = this\n    override operator fun times(multiplier: Double): Duration = this\n    override operator fun div(denominator: Int): Duration = this\n    override operator fun div(denominator: Long): Duration = this\n    override operator fun div(denominator: Float): Duration = this\n    override operator fun div(denominator: Double): Duration = this\n}\n\nprivate object DurationInfinitePositive : DurationInfinite() {\n    override val inYears: Double = Double.POSITIVE_INFINITY\n    override val inMonths: Double = Double.POSITIVE_INFINITY\n    override val inWeeks: Double = Double.POSITIVE_INFINITY\n    override val inDays: Double = Double.POSITIVE_INFINITY\n    override val inHours: Double = Double.POSITIVE_INFINITY\n    override val inMinutes: Double = Double.POSITIVE_INFINITY\n    override val inSeconds: Double = Double.POSITIVE_INFINITY\n    override val inMilliseconds: Double = Double.POSITIVE_INFINITY\n    override val inMicroseconds: Double = Double.POSITIVE_INFINITY\n    override val inNanoseconds: Long = Long.MAX_VALUE\n\n    override fun toString(): String {\n        return \"INFINITE\"\n    }\n\n    override operator fun unaryMinus(): Duration = DurationInfiniteNegative\n}\n\nprivate object DurationInfiniteNegative : DurationInfinite() {\n    override val inYears: Double = Double.NEGATIVE_INFINITY\n    override val inMonths: Double = Double.NEGATIVE_INFINITY\n    override val inWeeks: Double = Double.NEGATIVE_INFINITY\n    override val inDays: Double = Double.NEGATIVE_INFINITY\n    override val inHours: Double = Double.NEGATIVE_INFINITY\n    override val inMinutes: Double = Double.NEGATIVE_INFINITY\n    override val inSeconds: Double = Double.NEGATIVE_INFINITY\n    override val inMilliseconds: Double = Double.NEGATIVE_INFINITY\n    override val inMicroseconds: Double = Double.NEGATIVE_INFINITY\n    override val inNanoseconds: Long = Long.MIN_VALUE\n\n    override fun toString(): String {\n        return \"Duration(NEGATIVE_INFINITE)\"\n    }\n\n    override operator fun unaryMinus(): Duration = DurationInfiniteNegative\n}\n\nprivate class DurationNanoseconds(nanoseconds: Long) : Duration() {\n    override val inYears by lazy { (inDays / 365.25) }\n    override val inMonths by lazy { (inYears * 12) }\n    override val inWeeks by lazy { inDays / 7 }\n    override val inDays by lazy { inHours / 24 }\n    override val inHours by lazy { inMinutes / 60 }\n    override val inMinutes by lazy { inSeconds / 60 }\n    override val inSeconds by lazy { inMilliseconds / 1000 }\n    override val inMilliseconds by lazy { inMicroseconds / 1000 }\n    override val inMicroseconds by lazy { inNanoseconds / 1000.0 }\n    override val inNanoseconds = nanoseconds\n\n    companion object {\n        fun fromYears(years: Double) = fromDays(years * 365.25)\n        fun fromMonths(months: Double) = fromYears(months / 12)\n        fun fromWeeks(weeks: Double) = fromDays(weeks * 7)\n        fun fromDays(days: Double) = fromHours(days * 24)\n        fun fromHours(hours: Double) = fromMinutes(hours * 60)\n        fun fromMinutes(minutes: Double) = fromSeconds(minutes * 60)\n        fun fromSeconds(seconds: Double) = fromMilliseconds(seconds * 1000)\n        fun fromMilliseconds(milliseconds: Double) = fromMicroseconds(milliseconds * 1000)\n        fun fromMicroseconds(microseconds: Double) = fromNanoseconds((microseconds * 1000).roundToLong())\n        fun fromNanoseconds(nanoseconds: Long) = DurationNanoseconds(nanoseconds)\n    }\n}\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Int.years get(): Duration = this.toDouble().years\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Int.months get(): Duration = this.toDouble().months\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Int.weeks get(): Duration = this.toDouble().weeks\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Int.days get(): Duration = this.toDouble().days\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Int.hours get(): Duration = this.toDouble().hours\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Int.minutes get(): Duration = this.toDouble().minutes\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Int.seconds get(): Duration = this.toDouble().seconds\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Int.milliseconds get(): Duration = this.toDouble().milliseconds\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Int.microseconds get(): Duration = this.toDouble().microseconds\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Int.nanoseconds get(): Duration = this.toLong().nanoseconds\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Long.years get(): Duration = this.toDouble().years\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Long.months get(): Duration = this.toDouble().months\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Long.weeks get(): Duration = this.toDouble().weeks\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Long.days get(): Duration = this.toDouble().days\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Long.hours get(): Duration = this.toDouble().hours\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Long.minutes get(): Duration = this.toDouble().minutes\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Long.seconds get(): Duration = this.toDouble().seconds\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Long.milliseconds get(): Duration = this.toDouble().milliseconds\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Long.microseconds get(): Duration = this.toDouble().microseconds\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Long.nanoseconds get(): Duration = DurationNanoseconds.fromNanoseconds(this)\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Float.years get(): Duration = this.toDouble().years\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Float.months get(): Duration = this.toDouble().months\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Float.weeks get(): Duration = this.toDouble().weeks\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Float.days get(): Duration = this.toDouble().days\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Float.hours get(): Duration = this.toDouble().hours\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Float.minutes get(): Duration = this.toDouble().minutes\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Float.seconds get(): Duration = this.toDouble().seconds\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Float.milliseconds get(): Duration = this.toDouble().milliseconds\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Float.microseconds get(): Duration = this.toDouble().microseconds\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Float.nanoseconds get(): Duration = this.roundToLong().nanoseconds\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Double.years get(): Duration = DurationNanoseconds.fromYears(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Double.months get(): Duration = DurationNanoseconds.fromMonths(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Double.weeks get(): Duration = DurationNanoseconds.fromWeeks(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Double.days get(): Duration = DurationNanoseconds.fromDays(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Double.hours get(): Duration = DurationNanoseconds.fromHours(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Double.minutes get(): Duration = DurationNanoseconds.fromMinutes(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Double.seconds get(): Duration = DurationNanoseconds.fromSeconds(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Double.milliseconds get(): Duration = DurationNanoseconds.fromMilliseconds(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Double.microseconds get(): Duration = DurationNanoseconds.fromMicroseconds(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval Double.nanoseconds get(): Duration = this.roundToLong().nanoseconds\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun min(duration1: Duration, duration2: Duration): Duration =\n    when {\n        duration1 is DurationInfinitePositive -> duration2\n        duration1 is DurationInfiniteNegative -> duration1\n        duration2 is DurationInfinitePositive -> duration1\n        duration2 is DurationInfiniteNegative -> duration2\n        else -> kotlin.math.min(duration1.inNanoseconds, duration2.inNanoseconds).nanoseconds\n    }\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun max(duration1: Duration, duration2: Duration): Duration =\n    when {\n        duration1 is DurationInfinitePositive -> duration1\n        duration1 is DurationInfiniteNegative -> duration2\n        duration2 is DurationInfinitePositive -> duration2\n        duration2 is DurationInfiniteNegative -> duration1\n        else -> kotlin.math.max(duration1.inNanoseconds, duration2.inNanoseconds).nanoseconds\n    }\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/time/Rate.kt",
    "content": "package com.getbouncer.scan.framework.time\n\n/**\n * A rate of execution.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class Rate(val amount: Long, val duration: Duration) : Comparable<Rate> {\n    override fun compareTo(other: Rate): Int {\n        return (other.duration / other.amount).compareTo(duration / amount)\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/time/Timer.kt",
    "content": "package com.getbouncer.scan.framework.time\n\nimport android.util.Log\nimport com.getbouncer.scan.framework.Config\nimport kotlinx.coroutines.runBlocking\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsealed class Timer {\n\n    companion object {\n        @Deprecated(\n            message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n            replaceWith = ReplaceWith(\"StripeCardScan\"),\n        )\n        fun newInstance(\n            tag: String,\n            name: String,\n            updateInterval: Duration = 2.seconds,\n            enabled: Boolean = Config.isDebug\n        ) = if (enabled) {\n            LoggingTimer(\n                tag,\n                name,\n                updateInterval\n            )\n        } else {\n            NoOpTimer\n        }\n    }\n\n    /**\n     * Log the duration of a single task and return the result from that task.\n     *\n     * TODO: use contracts when they are no longer experimental\n     */\n    fun <T> measure(taskName: String? = null, task: () -> T): T {\n        // contract { callsInPlace(task, EXACTLY_ONCE) }\n        return runBlocking { measureSuspend(taskName) { task() } }\n    }\n\n    abstract suspend fun <T> measureSuspend(taskName: String? = null, task: suspend () -> T): T\n}\n\nprivate object NoOpTimer : Timer() {\n\n    // TODO: use contracts when they are no longer experimental\n    override suspend fun <T> measureSuspend(taskName: String?, task: suspend () -> T): T {\n        // contract { callsInPlace(task, EXACTLY_ONCE) }\n        return task()\n    }\n}\n\nprivate class LoggingTimer(\n    private val tag: String,\n    private val name: String,\n    private val updateInterval: Duration\n) : Timer() {\n    private var executionCount = 0\n    private var executionTotalDuration = Duration.ZERO\n    private var updateClock = Clock.markNow()\n\n    // TODO: use contracts when they are no longer experimental\n    override suspend fun <T> measureSuspend(taskName: String?, task: suspend () -> T): T {\n        // contract { callsInPlace(task, EXACTLY_ONCE) }\n        val (duration, result) = measureTime { task() }\n\n        executionCount++\n        executionTotalDuration += duration\n\n        if (updateClock.elapsedSince() > updateInterval) {\n            updateClock = Clock.markNow()\n            Log.d(\n                tag,\n                \"$name${if (!taskName.isNullOrEmpty()) \".$taskName\" else \"\"} executing on \" +\n                    \"thread ${Thread.currentThread().name} \" +\n                    \"AT ${executionCount / executionTotalDuration.inSeconds} FPS, \" +\n                    \"${executionTotalDuration.inMilliseconds / executionCount} MS/F\"\n            )\n        }\n        return result\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/util/AppDetails.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport android.content.Context\nimport com.getbouncer.scan.framework.BuildConfig\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class AppDetails(\n    val appPackageName: String?,\n    val applicationId: String,\n    val libraryPackageName: String,\n    val sdkVersion: String,\n    val sdkVersionCode: Int,\n    val sdkFlavor: String,\n    val isDebugBuild: Boolean\n) {\n    companion object {\n        @JvmStatic\n        fun fromContext(context: Context) = AppDetails(\n            appPackageName = getAppPackageName(context),\n            applicationId = getApplicationId(),\n            libraryPackageName = getLibraryPackageName(),\n            sdkVersion = getSdkVersion(),\n            sdkVersionCode = getSdkVersionCode(),\n            sdkFlavor = getSdkFlavor(),\n            isDebugBuild = isDebugBuild()\n        )\n    }\n}\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun getAppPackageName(context: Context): String? = context.applicationContext.packageName\n\nprivate fun getApplicationId(): String = \"\" // no longer available in later versions of gradle.\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun getLibraryPackageName(): String = BuildConfig.LIBRARY_PACKAGE_NAME\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun getSdkVersion(): String = BuildConfig.SDK_VERSION_STRING\n\nprivate fun getSdkVersionCode(): Int = -1 // no longer available in later versions of gradle.\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun getSdkFlavor(): String = BuildConfig.BUILD_TYPE\n\nprivate fun isDebugBuild(): Boolean = BuildConfig.DEBUG\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/util/ArrayExtensions.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport androidx.annotation.CheckResult\nimport java.nio.ByteBuffer\nimport kotlin.math.max\nimport kotlin.math.min\n\n/**\n * Update an array in place with a modifier function.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun <T> Array<T>.updateEach(operation: (original: T) -> T) {\n    for (i in this.indices) {\n        this[i] = operation(this[i])\n    }\n}\n\n/**\n * Update a [FloatArray] in place with a modifier function.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun FloatArray.updateEach(operation: (original: Float) -> Float) {\n    for (i in this.indices) {\n        this[i] = operation(this[i])\n    }\n}\n\n/**\n * Filter an array to only those values specified in an index array.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ninline fun <reified T> Array<T>.filterByIndexes(indexesToKeep: IntArray) =\n    Array(indexesToKeep.size) { this[indexesToKeep[it]] }\n\n/**\n * Filter an array to only those values specified in an index array.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun FloatArray.filterByIndexes(indexesToKeep: IntArray) =\n    FloatArray(indexesToKeep.size) { this[indexesToKeep[it]] }\n\n/**\n * Flatten an array of arrays into a single array of sequential values.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Array<FloatArray>.flatten() = if (this.isNotEmpty()) {\n    this.reshape(this.size * this[0].size)[0]\n} else {\n    floatArrayOf()\n}\n\n/**\n * Transpose an array of float arrays.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Array<FloatArray>.transpose() = if (this.isNotEmpty()) {\n    val oldRows = this.size\n    val oldColumns = this[0].size\n    Array(oldColumns) { newRow -> FloatArray(oldRows) { newColumn -> this[newColumn][newRow] } }\n} else {\n    this\n}\n\n/**\n * Reshape a two-dimensional array. Assume all rows of the original array are the same length, and\n * that the array is evenly divisible by the new columns.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Array<FloatArray>.reshape(newColumns: Int): Array<FloatArray> {\n    val oldRows = this.size\n    val oldColumns = if (this.isNotEmpty()) this[0].size else 0\n    val linearSize = oldRows * oldColumns\n    val newRows = linearSize / newColumns + if (linearSize % newColumns != 0) 1 else 0\n\n    var oldRow = 0\n    var oldColumn = 0\n    return Array(newRows) {\n        FloatArray(newColumns) {\n            val value = this[oldRow][oldColumn]\n            if (++oldColumn == oldColumns) {\n                oldColumn = 0\n                oldRow++\n            }\n            value\n        }\n    }\n}\n\n/**\n * Clamp the value between min and max\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun clamp(value: Float, minimum: Float, maximum: Float): Float =\n    max(minimum, min(maximum, value))\n\n/**\n * Return a list of indexes that pass the filter.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun FloatArray.filteredIndexes(predicate: (Float) -> Boolean): IntArray {\n    val filteredIndexes = ArrayList<Int>()\n    for (index in this.indices) {\n        if (predicate(this[index])) {\n            filteredIndexes.add(index)\n        }\n    }\n    return filteredIndexes.toIntArray()\n}\n\n/**\n * Divide a [ByteArray] into an array of byte arrays of a given size. If the original array is not\n * evenly divisible by the [chunkSize], the last ByteArray may be smaller than the chunk size.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun ByteArray.chunk(chunkSize: Int): Array<ByteArray> =\n    Array(this.size / chunkSize + if (this.size % chunkSize == 0) 0 else 1) {\n        copyOfRange(it * chunkSize, min((it + 1) * chunkSize, this.size))\n    }\n\n/**\n * Find the index of the maximum value in the array.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun FloatArray.indexOfMax(): Int? {\n    if (isEmpty()) {\n        return null\n    }\n\n    var maxIndex = 0\n    var maxValue = this[maxIndex]\n    for (index in this.indices) {\n        if (this[index] > maxValue) {\n            maxIndex = index\n            maxValue = this[index]\n        }\n    }\n\n    return maxIndex\n}\n\n/**\n * Convert a [ByteBuffer] to a [ByteArray].\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun ByteBuffer.toByteArray() = ByteArray(remaining()).also { this.get(it) }\n\n/**\n * Convert a list of [ByteBuffer]s to a single [ByteArray].\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun List<ByteBuffer>.toByteArray(): ByteArray {\n    val totalSize = this.sumOf { it.remaining() }\n    var offset = 0\n    return ByteArray(totalSize).apply {\n        // This should be using this@toByteArray.forEach, but doing so seems to require API 24. It's unclear why this\n        // won't use the kotlin.collections version of `forEach`, but it's not during compile.\n        for (it in this@toByteArray) {\n            val size = it.remaining()\n            it.get(this, offset, size)\n            offset += size\n        }\n    }\n}\n\n/**\n * Map an array to a new [Array].\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ninline fun <T, reified U> Array<T>.mapArray(transform: (T) -> U) = Array(this.size) { transform(this[it]) }\n\n/**\n * Map an array to a new [IntArray].\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun <T> Array<T>.mapToIntArray(transform: (T) -> Int) = IntArray(this.size) { transform(this[it]) }\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/util/Device.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.os.Build\nimport android.provider.Settings\nimport android.telephony.TelephonyManager\nimport java.util.Locale\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class Device(\n    val ids: DeviceIds,\n    val name: String,\n    val bootCount: Int,\n    val locale: String?,\n    val carrier: String?,\n    val networkOperator: String?,\n    val phoneType: Int?,\n    val phoneCount: Int,\n    val osVersion: Int,\n    val platform: String\n) {\n    companion object {\n        private val getDeviceDetails = cacheFirstResult { context: Context ->\n            Device(\n                ids = DeviceIds.fromContext(context),\n                name = getDeviceName(),\n                bootCount = getDeviceBootCount(context),\n                locale = getDeviceLocale(),\n                carrier = getDeviceCarrier(context),\n                networkOperator = getNetworkOperator(context),\n                phoneType = getDevicePhoneType(context),\n                phoneCount = getDevicePhoneCount(context),\n                osVersion = getOsVersion(),\n                platform = getPlatform()\n            )\n        }\n\n        @JvmStatic\n        fun fromContext(context: Context) = getDeviceDetails(context.applicationContext)\n    }\n}\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ndata class DeviceIds(\n    val androidId: String?\n) {\n    companion object {\n        private val getDeviceIds = cacheFirstResult { context: Context ->\n            DeviceIds(\n                androidId = getAndroidId(context)\n            )\n        }\n\n        fun fromContext(context: Context) = getDeviceIds(context.applicationContext)\n    }\n}\n\n@SuppressLint(\"HardwareIds\")\nprivate fun getAndroidId(context: Context): String? =\n    Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)\n\nprivate fun getDeviceBootCount(context: Context): Int =\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n        try {\n            Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT)\n        } catch (t: Throwable) {\n            -1\n        }\n    } else {\n        -1\n    }\n\nprivate fun getDeviceLocale(): String = \"${Locale.getDefault().isO3Language}_${Locale.getDefault().isO3Country}\"\n\nprivate fun getDeviceCarrier(context: Context) = try {\n    (context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?)?.networkOperatorName\n} catch (t: Throwable) {\n    null\n}\n\nprivate fun getDevicePhoneType(context: Context) = try {\n    (context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?)?.phoneType\n} catch (t: Throwable) {\n    null\n}\n\nprivate fun getDevicePhoneCount(context: Context) =\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n        try {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n                (context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?)?.activeModemCount ?: -1\n            } else {\n                @Suppress(\"deprecation\")\n                (context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?)?.phoneCount ?: -1\n            }\n        } catch (t: Throwable) {\n            -1\n        }\n    } else {\n        -1\n    }\n\nprivate fun getNetworkOperator(context: Context) =\n    (context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?)?.networkOperator\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun getOsVersion() = Build.VERSION.SDK_INT\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun getPlatform() = \"android\"\n\n/**\n * from https://stackoverflow.com/a/27836910/947883\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun getDeviceName(): String {\n    // TODO: change this back once we can support newer kotlin versions\n//    val manufacturer = Build.MANUFACTURER?.lowercase() ?: \"\"\n//    val model = Build.MODEL?.lowercase() ?: \"\"\n    val manufacturer = Build.MANUFACTURER?.toLowerCase(Locale.ROOT) ?: \"\"\n    val model = Build.MODEL?.toLowerCase(Locale.ROOT) ?: \"\"\n    return if (model.startsWith(manufacturer)) {\n        model\n    } else {\n        \"$manufacturer $model\"\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/util/File.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport java.io.File\nimport java.io.FileInputStream\nimport java.io.IOException\nimport java.lang.Exception\nimport java.security.MessageDigest\nimport java.security.NoSuchAlgorithmException\n\nprivate val illegalFileNameCharacters = setOf('\"', '*', '/', ':', '<', '>', '?', '\\\\', '|', '+', ',', ';', '=', '[', ']')\n\n/**\n * Sanitize the name of a file for storage\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nfun sanitizeFileName(unsanitized: String) =\n    unsanitized.map { char -> if (char in illegalFileNameCharacters) \"_\" else char }.joinToString(\"\")\n\n/**\n * Determine if a [File] matches the expected [hash].\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nsuspend fun fileMatchesHash(localFile: File, hash: String, hashAlgorithm: String) = try {\n    hash == calculateHash(localFile, hashAlgorithm)\n} catch (t: Throwable) {\n    false\n}\n\n/**\n * Calculate the hash of a file using the [hashAlgorithm].\n */\n@Throws(IOException::class, NoSuchAlgorithmException::class)\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nsuspend fun calculateHash(file: File, hashAlgorithm: String): String? =\n    withContext(Dispatchers.IO) {\n        if (file.exists()) {\n            val digest = MessageDigest.getInstance(hashAlgorithm)\n            FileInputStream(file).use { digest.update(it.readBytes()) }\n            digest.digest().joinToString(\"\") { \"%02x\".format(it) }\n        } else {\n            null\n        }\n    }\n\n/**\n * A file does not match the expected hash value.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass HashMismatchException(val algorithm: String, val expected: String, val actual: String?) :\n    Exception(\"Invalid hash result for algorithm '$algorithm'. Expected '$expected' but got '$actual'\") {\n    override fun toString() = \"HashMismatchException(algorithm='$algorithm', expected='$expected', actual='$actual')\"\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/util/FrameRateTracker.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport android.util.Log\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.time.Clock\nimport com.getbouncer.scan.framework.time.ClockMark\nimport com.getbouncer.scan.framework.time.Duration\nimport com.getbouncer.scan.framework.time.Rate\nimport com.getbouncer.scan.framework.time.seconds\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport java.util.concurrent.atomic.AtomicLong\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ninterface FrameRateListener {\n    fun onFrameRateUpdate(overallRate: Rate, instantRate: Rate)\n}\n\n/**\n * A class that tracks the rate at which frames are processed. This is useful for debugging to\n * determine how quickly a device is handling data.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass FrameRateTracker(\n    private val name: String,\n    private val listener: FrameRateListener? = null,\n    private val notifyInterval: Duration = 1.seconds,\n) {\n    private var firstFrameTime: ClockMark? = null\n    private var lastNotifyTime: ClockMark = Clock.markNow()\n\n    // This is -1 so that we do not calculate a rate for the first frame\n    private val totalFramesProcessed: AtomicLong = AtomicLong(-1)\n    private val framesProcessedSinceLastUpdate: AtomicLong = AtomicLong(0)\n\n    private val frameRateMutex = Mutex()\n\n    /**\n     * Calculate the current rate at which frames are being processed. If the notify interval has\n     * elapsed, notify the listener of the current rate.\n     */\n    suspend fun trackFrameProcessed() {\n        val totalFrames = totalFramesProcessed.incrementAndGet()\n        val framesSinceLastUpdate = framesProcessedSinceLastUpdate.incrementAndGet()\n\n        val lastNotifyTime = this.lastNotifyTime\n        val shouldNotifyOfFrameRate = totalFrames > 0 && frameRateMutex.withLock {\n            val shouldNotify = lastNotifyTime.elapsedSince() > notifyInterval\n            if (shouldNotify) {\n                this.lastNotifyTime = Clock.markNow()\n            }\n            shouldNotify\n        }\n\n        val firstFrameTime = this.firstFrameTime ?: Clock.markNow()\n        this.firstFrameTime = firstFrameTime\n\n        if (shouldNotifyOfFrameRate) {\n            val overallFrameRate = Rate(totalFrames, firstFrameTime.elapsedSince())\n            val instantFrameRate = Rate(framesSinceLastUpdate, lastNotifyTime.elapsedSince())\n\n            logProcessingRate(overallFrameRate, instantFrameRate)\n            listener?.onFrameRateUpdate(overallFrameRate, instantFrameRate)\n            framesProcessedSinceLastUpdate.set(0)\n        }\n    }\n\n    /**\n     * Reset the state of the frame rate tracker.\n     */\n    fun reset() {\n        firstFrameTime = null\n        lastNotifyTime = Clock.markNow()\n        totalFramesProcessed.set(0)\n        framesProcessedSinceLastUpdate.set(0)\n    }\n\n    /**\n     * Get the average frame rate for this device\n     */\n    fun getAverageFrameRate() = Rate(\n        amount = totalFramesProcessed.get(),\n        duration = firstFrameTime?.elapsedSince() ?: Duration.ZERO\n    )\n\n    /**\n     * The processing rate has been updated. This is useful for debugging and measuring performance.\n     *\n     * @param overallRate: The total frame rate at which the analyzer is running\n     * @param instantRate: The instantaneous frame rate at which the analyzer is running\n     */\n    private fun logProcessingRate(overallRate: Rate, instantRate: Rate) {\n        val overallFps = if (overallRate.duration != Duration.ZERO) {\n            overallRate.amount / overallRate.duration.inSeconds\n        } else {\n            0.0\n        }\n\n        val instantFps = if (instantRate.duration != Duration.ZERO) {\n            instantRate.amount / instantRate.duration.inSeconds\n        } else {\n            0.0\n        }\n\n        if (Config.isDebug) {\n            Log.d(Config.logTag, \"$name processing avg=$overallFps, inst=$instantFps\")\n        }\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/util/FrameSaver.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport androidx.annotation.CheckResult\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport java.util.LinkedList\n\n/**\n * Save data frames for later retrieval.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nabstract class FrameSaver<Identifier, Frame, MetaData> {\n\n    private val saveFrameMutex = Mutex()\n    private val savedFrames = mutableMapOf<Identifier, LinkedList<Frame>>()\n\n    /**\n     * Determine how frames should be classified using [getSaveFrameIdentifier], and then store them\n     * in a map of frames based on that identifier.\n     *\n     * This method keeps track of the total number of saved frames. If the total number or total\n     * size exceeds the maximum allowed, the oldest frames will be dropped.\n     */\n    suspend fun saveFrame(frame: Frame, metaData: MetaData) {\n        val identifier = getSaveFrameIdentifier(frame, metaData) ?: return\n        return saveFrameMutex.withLock {\n            val maxSavedFrames = getMaxSavedFrames(identifier)\n\n            val frames = savedFrames.getOrPut(identifier) { LinkedList() }\n            frames.addFirst(frame)\n\n            while (frames.size > maxSavedFrames) {\n                // saved frames is over size limit, reduce until it's not\n                removeFrame(identifier, frames)\n            }\n        }\n    }\n\n    /**\n     * Retrieve a copy of the list of saved frames.\n     */\n    @CheckResult\n    fun getSavedFrames(): Map<Identifier, LinkedList<Frame>> = savedFrames.toMap()\n\n    /**\n     * Clear all saved frames\n     */\n    suspend fun reset() = saveFrameMutex.withLock {\n        savedFrames.clear()\n    }\n\n    protected abstract fun getMaxSavedFrames(savedFrameIdentifier: Identifier): Int\n\n    /**\n     * Determine if a data frame should be saved for future processing.\n     *\n     * If this method returns a non-null string, the frame will be saved under that identifier.\n     */\n    protected abstract fun getSaveFrameIdentifier(frame: Frame, metaData: MetaData): Identifier?\n\n    /**\n     * Remove a frame from this list. The most recently added frames will be at the beginning of\n     * this list, while the least recently added frames will be at the end.\n     */\n    protected open fun removeFrame(identifier: Identifier, frames: LinkedList<Frame>) {\n        frames.removeLast()\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/util/ItemCounter.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport androidx.annotation.CheckResult\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport java.util.LinkedList\n\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\ninterface ItemCounter<T> {\n    suspend fun countItem(item: T): Int\n\n    fun getHighestCountItem(minCount: Int = 1): Pair<Int, T>?\n\n    suspend fun reset()\n}\n\n/**\n * A class that counts and saves items.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass ItemTotalCounter<T>(firstValue: T? = null) : ItemCounter<T> {\n    private val storageMutex = Mutex()\n    private val items = mutableMapOf<T, Int>()\n\n    init { if (firstValue != null) runBlocking { countItem(firstValue) } }\n\n    /**\n     * Increment the count for the given item. Return the new count for the given item.\n     */\n    override suspend fun countItem(item: T): Int = storageMutex.withLock {\n        1 + (items.put(item, 1 + (items[item] ?: 0)) ?: 0)\n    }\n\n    /**\n     * Get the item that with the highest count.\n     *\n     * @param minCount the minimum times an item must have been counted.\n     */\n    @CheckResult\n    override fun getHighestCountItem(minCount: Int): Pair<Int, T>? =\n        items\n            .maxByOrNull { it.value }\n            ?.let { if (items[it.key] ?: 0 >= minCount) it.value to it.key else null }\n\n    /**\n     * Reset all item counts.\n     */\n    override suspend fun reset() = storageMutex.withLock {\n        items.clear()\n    }\n}\n\n/**\n * A class that keeps track of [maxItemsToTrack] recent items.\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nclass ItemRecencyCounter<T>(\n    private val maxItemsToTrack: Int,\n    firstValue: T? = null\n) : ItemCounter<T> {\n    private val storageMutex = Mutex()\n    private val items = LinkedList<T>()\n\n    init { if (firstValue != null) runBlocking { countItem(firstValue) } }\n\n    /**\n     * Increment the count for the given item. Return the new count for the given item.\n     */\n    override suspend fun countItem(item: T): Int = storageMutex.withLock {\n        items.addFirst(item)\n\n        while (items.size > maxItemsToTrack) {\n            items.removeLast()\n        }\n\n        items.count { it == item }\n    }\n\n    /**\n     * Get the item that with the highest count.\n     *\n     * @param minCount the minimum times an item must have been counted.\n     */\n    @CheckResult\n    override fun getHighestCountItem(minCount: Int): Pair<Int, T>? =\n        items\n            .groupingBy { it }\n            .eachCount()\n            .filter { it.value >= minCount }\n            .maxByOrNull { it.value }\n            ?.let { it.value to it.key }\n\n    /**\n     * Reset all item counts.\n     */\n    override suspend fun reset() = storageMutex.withLock {\n        items.clear()\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/util/Layout.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport android.graphics.Rect\nimport android.graphics.RectF\nimport android.util.Size\nimport android.util.SizeF\nimport android.view.View\nimport androidx.annotation.CheckResult\nimport kotlin.math.max\nimport kotlin.math.min\nimport kotlin.math.roundToInt\n\n/**\n * Determine the maximum size of rectangle with a given aspect ratio (X/Y) that can fit inside the\n * specified area.\n *\n * For example, if the aspect ratio is 1/2 and the area is 2x2, the resulting rectangle would be\n * size 1x2 and look like this:\n * ```\n *  ________\n * | |    | |\n * | |    | |\n * | |    | |\n * |_|____|_|\n * ```\n */\n@CheckResult\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nfun maxAspectRatioInSize(area: Size, aspectRatio: Float): Size {\n    var width = area.width\n    var height = (width / aspectRatio).roundToInt()\n\n    return if (height <= area.height) {\n        Size(area.width, height)\n    } else {\n        height = area.height\n        width = (height * aspectRatio).roundToInt()\n        Size(min(width, area.width), height)\n    }\n}\n\n/**\n * Determine the minimum size of rectangle with a given aspect ratio (X/Y) that a specified area\n * can fit inside.\n *\n * For example, if the aspect ratio is 1/2 and the area is 1x1, the resulting rectangle would be\n * size 1x2 and look like this:\n * ```\n *  ____\n * |____|\n * |    |\n * |____|\n * |____|\n * ```\n */\n@CheckResult\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nfun minAspectRatioSurroundingSize(area: Size, aspectRatio: Float): Size {\n    var width = area.width\n    var height = (width / aspectRatio).roundToInt()\n\n    return if (height >= area.height) {\n        Size(area.width, height)\n    } else {\n        height = area.height\n        width = (height * aspectRatio).roundToInt()\n        Size(max(width, area.width), height)\n    }\n}\n\n/**\n * Given a size and an aspect ratio, resize the area to fit that aspect ratio. If the desired aspect\n * ratio is smaller than the one of the provided size, the size will be cropped to match. If the\n * desired aspect ratio is larger than the that of the provided size, then the size will be expanded\n * to match.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun adjustSizeToAspectRatio(area: Size, aspectRatio: Float): Size = if (aspectRatio < 1) {\n    Size(area.width, (area.width / aspectRatio).roundToInt())\n} else {\n    Size((area.height * aspectRatio).roundToInt(), area.height)\n}\n\n/**\n * Calculate the position of the [Size] within the [containingSize]. This makes a few assumptions:\n * 1. the [Size] and the [containingSize] are centered relative to each other.\n * 2. the [Size] and the [containingSize] have the same orientation\n * 3. the [containingSize] and the [Size] share either a horizontal or vertical field of view\n * 4. the non-shared field of view must be smaller on the [Size] than the [containingSize]\n *\n * If using this to project a preview image onto a full camera image, This makes a few assumptions:\n * 1. the preview image [Size] and full image [containingSize] are centered relative to each other\n * 2. the preview image and the full image have the same orientation\n * 3. the preview image and the full image share either a horizontal or vertical field of view\n * 4. the non-shared field of view must be smaller on the preview image than the full image\n *\n * Note that the [Size] and the [containingSize] are allowed to have completely independent\n * resolutions.\n */\n@CheckResult\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nfun Size.scaleAndCenterWithin(containingSize: Size): Rect {\n    val aspectRatio = width.toFloat() / height\n\n    // Since the preview image may be at a different resolution than the full image, scale the\n    // preview image to be circumscribed by the fullImage.\n    val scaledSize = maxAspectRatioInSize(containingSize, aspectRatio)\n    val left = (containingSize.width - scaledSize.width) / 2\n    val top = (containingSize.height - scaledSize.height) / 2\n    return Rect(\n        left,\n        top,\n        left + scaledSize.width,\n        top + scaledSize.height,\n    )\n}\n\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Size.scaleAndCenterWithin(containingRect: Rect): Rect =\n    this.scaleAndCenterWithin(containingRect.size()).move(containingRect.left, containingRect.top)\n\n/**\n * Calculate the position of the [Size] surrounding the [surroundedSize]. This makes a few\n * assumptions:\n * 1. the [Size] and the [surroundedSize] are centered relative to each other.\n * 2. the [Size] and the [surroundedSize] have the same orientation\n * 3. the [surroundedSize] and the [Size] share either a horizontal or vertical field of view\n * 4. the non-shared field of view must be smaller on the [surroundedSize] than the [Size]\n *\n * If using this to project a full camera image onto a preview image, This makes a few assumptions:\n * 1. the preview image [surroundedSize] and full image [Size] are centered relative to each other\n * 2. the preview image and the full image have the same orientation\n * 3. the preview image and the full image share either a horizontal or vertical field of view\n * 4. the non-shared field of view must be smaller on the preview image than the full image\n *\n * Note that the [Size] and the [surroundedSize] are allowed to have completely independent\n * resolutions.\n */\n@CheckResult\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nfun Size.scaleAndCenterSurrounding(surroundedSize: Size): Rect {\n    val aspectRatio = width.toFloat() / height\n\n    val scaledSize = minAspectRatioSurroundingSize(surroundedSize, aspectRatio)\n    val left = (surroundedSize.width - scaledSize.width) / 2\n    val top = (surroundedSize.height - scaledSize.height) / 2\n    return Rect(\n        left,\n        top,\n        left + scaledSize.width,\n        top + scaledSize.height,\n    )\n}\n\n/**\n * Scale a size based on percentage scale values, and keep track of its position.\n */\n@CheckResult\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nfun Size.scaleCentered(x: Float, y: Float): Rect {\n    val newSize = this.scale(x, y)\n    val left = (this.width - newSize.width) / 2\n    val top = (this.height - newSize.height) / 2\n    return Rect(\n        left,\n        top,\n        left + newSize.width,\n        top + newSize.height,\n    )\n}\n\n/**\n * Calculate the new size based on percentage scale values.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeF.scale(x: Float, y: Float) = SizeF(this.width * x, this.height * y)\n\n/**\n * Calculate the new size based on a percentage scale.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeF.scale(scale: Float) = this.scale(scale, scale)\n\n/**\n * Calculate the new size based on percentage scale values.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Size.scale(x: Float, y: Float): Size = Size((this.width * x).roundToInt(), (this.height * y).roundToInt())\n\n/**\n * Calculate the new size based on a percentage scale.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Size.scale(scale: Float) = this.scale(scale, scale)\n\n/**\n * Center a size on a given rectangle. The size may be larger or smaller than the rect.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Size.centerOn(rect: Rect) = Rect(\n    /* left */\n    rect.centerX() - this.width / 2,\n    /* top */\n    rect.centerY() - this.height / 2,\n    /* right */\n    rect.centerX() + this.width / 2,\n    /* bottom */\n    rect.centerY() + this.height / 2\n)\n\n/**\n * Scale a [Rect] to have a size equivalent to the [scaledSize]. This will also scale the position\n * of the [Rect].\n *\n * For example, scaling a Rect(1, 2, 3, 4) by Size(5, 6) will result in a Rect(5, 12, 15, 24)\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectF.scaled(scaledSize: Size) = RectF(\n    this.left * scaledSize.width,\n    this.top * scaledSize.height,\n    this.right * scaledSize.width,\n    this.bottom * scaledSize.height\n)\n\n/**\n * Scale a [Rect] to have a size equivalent to the [scaledSize]. This will maintain the center\n * position of the [Rect].\n *\n * For example, scaling a Rect(5, 6, 7, 8) by Size(2, 0.5) will result\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectF.centerScaled(scaleX: Float, scaleY: Float) = RectF(\n    this.centerX() - this.width() * scaleX / 2,\n    this.centerY() - this.height() * scaleY / 2,\n    this.centerX() + this.width() * scaleX / 2,\n    this.centerY() + this.height() * scaleY / 2\n)\n\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Rect.centerScaled(scaleX: Float, scaleY: Float) = Rect(\n    this.centerX() - (this.width() * scaleX / 2).toInt(),\n    this.centerY() - (this.height() * scaleY / 2).toInt(),\n    this.centerX() + (this.width() * scaleX / 2).toInt(),\n    this.centerY() + (this.height() * scaleY / 2).toInt()\n)\n\n/**\n * Converts a size to rectangle with the top left corner at 0,0\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Size.toRect() = Rect(0, 0, this.width, this.height)\n\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Size.toRectF() = RectF(0F, 0F, this.width.toFloat(), this.height.toFloat())\n\n/**\n * Transpose a size's width and height.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Size.transpose() = Size(this.height, this.width)\n\n/**\n * Return a rect that is the intersection of two other rects\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Rect.intersectionWith(rect: Rect): Rect {\n    require(this.intersect(rect)) {\n        \"Given rects do not intersect $this <> $rect\"\n    }\n\n    return Rect(\n        max(this.left, rect.left),\n        max(this.top, rect.top),\n        min(this.right, rect.right),\n        min(this.bottom, rect.bottom)\n    )\n}\n\n/**\n * Move relative to its current position\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Rect.move(relativeX: Int, relativeY: Int) = Rect(\n    this.left + relativeX,\n    this.top + relativeY,\n    this.right + relativeX,\n    this.bottom + relativeY,\n)\n\n/**\n * Move relative to its current position\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectF.move(relativeX: Float, relativeY: Float) = RectF(\n    this.left + relativeX,\n    this.top + relativeY,\n    this.right + relativeX,\n    this.bottom + relativeY,\n)\n\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Size.toSizeF() = SizeF(width.toFloat(), height.toFloat())\n\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeF.toSize() = Size(width.roundToInt(), height.roundToInt())\n\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Rect.toRectF() = RectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())\n\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectF.toRect() = Rect(left.roundToInt(), top.roundToInt(), right.roundToInt(), bottom.roundToInt())\n\n/**\n * Takes a relation between a region of interest and a size and projects the region of interest\n * to that new location\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeF.projectRegionOfInterest(toSize: SizeF, regionOfInterest: RectF): RectF {\n    require(this.width > 0 && this.height > 0) {\n        \"Cannot project from container with non-positive dimensions\"\n    }\n\n    return RectF(\n        regionOfInterest.left * toSize.width / this.width,\n        regionOfInterest.top * toSize.height / this.height,\n        regionOfInterest.right * toSize.width / this.width,\n        regionOfInterest.bottom * toSize.height / this.height,\n    )\n}\n\n/**\n * Takes a relation between a region of interest and a size and projects the region of interest\n * to that new location\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Size.projectRegionOfInterest(toSize: Size, regionOfInterest: Rect) =\n    this.toSizeF().projectRegionOfInterest(toSize.toSizeF(), regionOfInterest.toRectF()).toRect()\n\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectF.projectRegionOfInterest(toSize: SizeF, regionOfInterest: RectF) =\n    this.size().projectRegionOfInterest(toSize, regionOfInterest.move(-this.left, -this.top))\n\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Rect.projectRegionOfInterest(toSize: Size, regionOfInterest: Rect) =\n    this.size().projectRegionOfInterest(toSize, regionOfInterest.move(-this.left, -this.top))\n\n/**\n * Project a region of interest from one [Rect] to another. For example, given the rect and region of interest:\n *  _______\n * |       |\n * |    _  |\n * |   |_| |\n * |       |\n * |_______|\n *\n * When projected to the following region:\n *  ___________\n * |           |\n * |           |\n * |___________|\n *\n * The position and size of the region of interest are scaled to the new rect.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectF.projectRegionOfInterest(toRect: RectF, regionOfInterest: RectF) =\n    this.projectRegionOfInterest(toRect.size(), regionOfInterest).move(toRect.left, toRect.top)\n\n/**\n * Project a region of interest from one [Rect] to another. For example, given the rect and region of interest:\n *  _______\n * |       |\n * |    _  |\n * |   |_| |\n * |       |\n * |_______|\n *\n * When projected to the following region:\n *  ___________\n * |           |\n * |           |\n * |___________|\n *\n * The position and size of the region of interest are scaled to the new rect.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Rect.projectRegionOfInterest(toRect: Rect, regionOfInterest: Rect) =\n    this.projectRegionOfInterest(toRect.size(), regionOfInterest).move(toRect.left, toRect.top)\n\n/**\n * This method allows relocating and resizing a portion of a [Size]. It returns the required\n * translations required to achieve this relocation. This is useful for zooming in on sections of\n * an image.\n *\n * For example, given a size 5x5 and an original region (2, 2, 3, 3):\n *\n *  _______\n * |       |\n * |   _   |\n * |  |_|  |\n * |       |\n * |_______|\n *\n * If the [newRegion] is (1, 1, 4, 4) and the [newSize] is 6x6, the result will look like this:\n *\n *  ________\n * |  ___   |\n * | |   |  |\n * | |   |  |\n * | |___|  |\n * |        |\n * |________|\n *\n * Nine individual translations will be returned for the affected regions. The returned [Rect]s\n * will look like this:\n *\n *  ________\n * |_|___|__|\n * | |   |  |\n * | |   |  |\n * |_|___|__|\n * | |   |  |\n * |_|___|__|\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Size.resizeRegion(\n    originalRegion: Rect,\n    newRegion: Rect,\n    newSize: Size\n): Map<Rect, Rect> = mapOf(\n    Rect(\n        0,\n        0,\n        originalRegion.left,\n        originalRegion.top\n    ) to Rect(\n        0,\n        0,\n        newRegion.left,\n        newRegion.top\n    ),\n    Rect(\n        originalRegion.left,\n        0,\n        originalRegion.right,\n        originalRegion.top\n    ) to Rect(\n        newRegion.left,\n        0,\n        newRegion.right,\n        newRegion.top\n    ),\n    Rect(\n        originalRegion.right,\n        0,\n        this.width,\n        originalRegion.top\n    ) to Rect(\n        newRegion.right,\n        0,\n        newSize.width,\n        newRegion.top\n    ),\n    Rect(\n        0,\n        originalRegion.top,\n        originalRegion.left,\n        originalRegion.bottom\n    ) to Rect(\n        0,\n        newRegion.top,\n        newRegion.left,\n        newRegion.bottom\n    ),\n    Rect(\n        originalRegion.left,\n        originalRegion.top,\n        originalRegion.right,\n        originalRegion.bottom\n    ) to Rect(\n        newRegion.left,\n        newRegion.top,\n        newRegion.right,\n        newRegion.bottom\n    ),\n    Rect(\n        originalRegion.right,\n        originalRegion.top,\n        this.width,\n        originalRegion.bottom\n    ) to Rect(\n        newRegion.right,\n        newRegion.top,\n        newSize.width,\n        newRegion.bottom\n    ),\n    Rect(\n        0,\n        originalRegion.bottom,\n        originalRegion.left,\n        this.height\n    ) to Rect(\n        0,\n        newRegion.bottom,\n        newRegion.left,\n        newSize.height\n    ),\n    Rect(\n        originalRegion.left,\n        originalRegion.bottom,\n        originalRegion.right,\n        this.height\n    ) to Rect(\n        newRegion.left,\n        newRegion.bottom,\n        newRegion.right,\n        newSize.height\n    ),\n    Rect(\n        originalRegion.right,\n        originalRegion.bottom,\n        this.width,\n        this.height\n    ) to Rect(\n        newRegion.right,\n        newRegion.bottom,\n        newSize.width,\n        newSize.height\n    )\n)\n\n/**\n * Determine the size of a [Rect].\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Rect.size() = Size(width(), height())\n\n/**\n * Determine the size of a [RectF].\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun RectF.size() = SizeF(width(), height())\n\n/**\n * Determine the aspect ratio of a [Size].\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Size.aspectRatio() = width.toFloat() / height.toFloat()\n\n/**\n * Determine the aspect ratio of a [SizeF].\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun SizeF.aspectRatio() = width / height\n\n/**\n * Determine the size of a [View].\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun View.size() = Size(width, height)\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/util/Memoize.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport com.getbouncer.scan.framework.time.Clock\nimport com.getbouncer.scan.framework.time.ClockMark\nimport com.getbouncer.scan.framework.time.Duration\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\n\n/**\n * A symbol for identifying values that have not yet been initialized.\n */\nprivate object UninitializedValue\n\n/**\n * A class that memoizes the result of a suspend function. Only one coroutine will ever perform the\n * work in the suspend function, others will suspend until a result is available, and then return\n * that result.\n */\nprivate class MemoizeSuspend0<out Result>(private val f: suspend () -> Result) {\n    private val initializeMutex = Mutex()\n\n    @Volatile private var value: Any? = UninitializedValue\n\n    fun memoize(): suspend () -> Result = {\n        initializeMutex.withLock {\n            if (value == UninitializedValue) {\n                value = f()\n            }\n            @Suppress(\"UNCHECKED_CAST\") (value as Result)\n        }\n    }\n}\n\n/**\n * A class that memoizes the result of a suspend function. Only one coroutine will ever perform the\n * work in the suspend function, others will suspend until a result is available, and then return\n * that result.\n */\nprivate class MemoizeSuspend1<in Input, out Result>(private val f: suspend (Input) -> Result) {\n    private val lookupMutex = Mutex()\n\n    private val values = mutableMapOf<Input, Result>()\n    private val mutexes = mutableMapOf<Input, Mutex>()\n\n    private suspend fun getMutex(input: Input): Mutex = lookupMutex.withLock {\n        mutexes.getOrPut(input) { Mutex() }\n    }\n\n    fun memoize(): suspend (Input) -> Result = { input ->\n        getMutex(input).withLock {\n            values.getOrPut(input) { f(input) }\n        }\n    }\n}\n\n/**\n * A class that memoizes the result of a suspend function. Only one coroutine will ever perform the\n * work in the suspend function, others will suspend until a result is available, and then return\n * that result.\n */\nprivate class MemoizeSuspend2<in Input1, in Input2, out Result>(\n    private val f: suspend (Input1, Input2) -> Result\n) {\n    private val lookupMutex = Mutex()\n\n    private val values = mutableMapOf<Pair<Input1, Input2>, Result>()\n    private val mutexes = mutableMapOf<Pair<Input1, Input2>, Mutex>()\n\n    private suspend fun getMutex(input1: Input1, input2: Input2): Mutex = lookupMutex.withLock {\n        mutexes.getOrPut(input1 to input2) { Mutex() }\n    }\n\n    fun memoize(): suspend (Input1, Input2) -> Result = { input1, input2 ->\n        getMutex(input1, input2).withLock {\n            values.getOrPut(input1 to input2) { f(input1, input2) }\n        }\n    }\n}\n\n/**\n * A class that memoizes the result of a suspend function. Only one coroutine will ever perform the\n * work in the suspend function, others will suspend until a result is available, and then return\n * that result.\n */\nprivate class MemoizeSuspend3<in Input1, in Input2, in Input3, out Result>(\n    private val f: suspend (Input1, Input2, Input3) -> Result\n) {\n    private val lookupMutex = Mutex()\n\n    private val values = mutableMapOf<Triple<Input1, Input2, Input3>, Result>()\n    private val mutexes = mutableMapOf<Triple<Input1, Input2, Input3>, Mutex>()\n\n    private suspend fun getMutex(input1: Input1, input2: Input2, input3: Input3): Mutex = lookupMutex.withLock {\n        mutexes.getOrPut(Triple(input1, input2, input3)) { Mutex() }\n    }\n\n    fun memoize(): suspend (Input1, Input2, Input3) -> Result = { input1, input2, input3 ->\n        getMutex(input1, input2, input3).withLock {\n            values.getOrPut(Triple(input1, input2, input3)) { f(input1, input2, input3) }\n        }\n    }\n}\n\n/**\n * A class that memoizes the result of a function. This method is threadsafe. Only one thread will\n * ever invoke the backing function, other threads will block until a result is available, and then\n * return that result. The result will expire after the defined timeout, at which point the function\n * can be executed again.\n */\nprivate class MemoizeSuspendExpiring0<out Result>(\n    private val validFor: Duration,\n    private val f: suspend () -> Result,\n) {\n    private val initializeMutex = Mutex()\n\n    @Volatile private var value: Any? = UninitializedValue\n    private var expiration: ClockMark? = null\n\n    fun memoize(): suspend () -> Result = {\n        initializeMutex.withLock {\n            if (value == UninitializedValue || expiration?.hasPassed() != false) {\n                value = f()\n                expiration = Clock.markNow() + validFor\n            }\n            @Suppress(\"UNCHECKED_CAST\") (value as Result)\n        }\n    }\n}\n\n/**\n * A class that memoizes the result of a function. This method is threadsafe. Only one thread will\n * ever invoke the backing function, other threads will block until a result is available, and then\n * return that result. The result will expire after the defined timeout, at which point the function\n * can be executed again.\n */\nprivate class MemoizeSuspendExpiring1<in Input, out Result>(\n    private val validFor: Duration,\n    private val f: suspend (Input) -> Result,\n) {\n    private val lookupMutex = Mutex()\n\n    private val values = mutableMapOf<Input, Pair<Result, ClockMark>>()\n    private val mutexes = mutableMapOf<Input, Mutex>()\n\n    private suspend fun getMutex(input: Input): Mutex = lookupMutex.withLock {\n        mutexes.getOrPut(input) { Mutex() }\n    }\n\n    fun memoize(): suspend (Input) -> Result = { input ->\n        getMutex(input).withLock {\n            val (result, expiration) = values[input] ?: UninitializedValue to null\n            if (result == UninitializedValue || expiration?.hasPassed() != false) {\n                val computedResult = f(input)\n                values[input] = computedResult to Clock.markNow() + validFor\n                computedResult\n            } else {\n                @Suppress(\"UNCHECKED_CAST\") (result as Result)\n            }\n        }\n    }\n}\n\n/**\n * A class that memoizes the result of a function. This method is threadsafe. Only one thread will\n * ever invoke the backing function, other threads will block until a result is available, and then\n * return that result. The result will expire after the defined timeout, at which point the function\n * can be executed again.\n */\nprivate class MemoizeSuspendExpiring2<in Input1, in Input2, out Result>(\n    private val validFor: Duration,\n    private val f: suspend (Input1, Input2) -> Result,\n) {\n    private val lookupMutex = Mutex()\n\n    private val values = mutableMapOf<Pair<Input1, Input2>, Pair<Result, ClockMark>>()\n    private val mutexes = mutableMapOf<Pair<Input1, Input2>, Mutex>()\n\n    private suspend fun getMutex(input1: Input1, input2: Input2): Mutex = lookupMutex.withLock {\n        mutexes.getOrPut(input1 to input2) { Mutex() }\n    }\n\n    fun memoize(): suspend (Input1, Input2) -> Result = { input1, input2 ->\n        getMutex(input1, input2).withLock {\n            val (result, expiration) = values[input1 to input2] ?: UninitializedValue to null\n            if (result == UninitializedValue || expiration?.hasPassed() != false) {\n                val computedResult = f(input1, input2)\n                values[input1 to input2] = computedResult to Clock.markNow() + validFor\n                computedResult\n            } else {\n                @Suppress(\"UNCHECKED_CAST\") (result as Result)\n            }\n        }\n    }\n}\n\n/**\n * A class that memoizes the result of a function. This method is threadsafe. Only one thread will\n * ever invoke the backing function, other threads will block until a result is available, and then\n * return that result. The result will expire after the defined timeout, at which point the function\n * can be executed again.\n */\nprivate class MemoizeSuspendExpiring3<in Input1, in Input2, in Input3, out Result>(\n    private val validFor: Duration,\n    private val f: suspend (Input1, Input2, Input3) -> Result,\n) {\n    private val values = mutableMapOf<Triple<Input1, Input2, Input3>, Pair<Result, ClockMark>>()\n    private val mutexes = mutableMapOf<Triple<Input1, Input2, Input3>, Mutex>()\n\n    @Synchronized\n    private fun getMutex(input1: Input1, input2: Input2, input3: Input3): Mutex =\n        mutexes.getOrPut(Triple(input1, input2, input3)) { Mutex() }\n\n    fun memoize(): suspend (Input1, Input2, Input3) -> Result = { input1, input2, input3 ->\n        getMutex(input1, input2, input3).withLock {\n            val (result, expiration) = values[Triple(input1, input2, input3)] ?: UninitializedValue to null\n            if (result == UninitializedValue || expiration?.hasPassed() != false) {\n                val computedResult = f(input1, input2, input3)\n                values[Triple(input1, input2, input3)] = computedResult to Clock.markNow() + validFor\n                computedResult\n            } else {\n                @Suppress(\"UNCHECKED_CAST\") (result as Result)\n            }\n        }\n    }\n}\n\n/**\n * A class that memoizes the result of a function. This method is threadsafe. Only one thread will\n * ever invoke the backing function, other threads will block until a result is available, and then\n * return that result.\n */\nprivate class Memoize0<out Result>(private val function: () -> Result) : () -> Result {\n    @Volatile private var value: Any? = UninitializedValue\n\n    @Synchronized\n    override fun invoke(): Result {\n        if (value == UninitializedValue) {\n            value = function()\n        }\n        @Suppress(\"UNCHECKED_CAST\") return (value as Result)\n    }\n}\n\n/**\n * A class that memoizes the result of a function. This method is threadsafe. Only one thread will\n * ever invoke the backing function, other threads will block until a result is available, and then\n * return that result.\n */\nprivate class Memoize1<in Input, out Result>(\n    private val function: (Input) -> Result\n) : (Input) -> Result {\n    private val values = mutableMapOf<Input, Result>()\n    private val locks = mutableMapOf<Input, Any>()\n\n    @Synchronized\n    private fun getLock(input: Input): Any = locks.getOrPut(input) { Object() }\n\n    override fun invoke(input: Input): Result {\n        val lock = getLock(input)\n        return synchronized(lock) {\n            values.getOrPut(input) { function(input) }\n        }\n    }\n}\n\n/**\n * A class that memoizes the result of a function. This method is threadsafe. Only one thread will\n * ever invoke the backing function, other threads will block until a result is available, and then\n * return that result.\n */\nprivate class Memoize2<in Input1, in Input2, out Result>(\n    private val function: (Input1, Input2) -> Result\n) : (Input1, Input2) -> Result {\n    private val values = mutableMapOf<Pair<Input1, Input2>, Result>()\n    private val locks = mutableMapOf<Pair<Input1, Input2>, Any>()\n\n    @Synchronized\n    private fun getLock(input1: Input1, input2: Input2): Any =\n        locks.getOrPut(input1 to input2) { Object() }\n\n    override fun invoke(input1: Input1, input2: Input2): Result {\n        val lock = getLock(input1, input2)\n        return synchronized(lock) {\n            values.getOrPut(input1 to input2) { function(input1, input2) }\n        }\n    }\n}\n\n/**\n * A class that memoizes the result of a function. This method is threadsafe. Only one thread will\n * ever invoke the backing function, other threads will block until a result is available, and then\n * return that result.\n */\nprivate class Memoize3<in Input1, in Input2, in Input3, out Result>(\n    private val function: (Input1, Input2, Input3) -> Result\n) : (Input1, Input2, Input3) -> Result {\n    private val values = mutableMapOf<Triple<Input1, Input2, Input3>, Result>()\n    private val locks = mutableMapOf<Triple<Input1, Input2, Input3>, Any>()\n\n    @Synchronized\n    private fun getLock(input1: Input1, input2: Input2, input3: Input3): Any =\n        locks.getOrPut(Triple(input1, input2, input3)) { Object() }\n\n    override fun invoke(input1: Input1, input2: Input2, input3: Input3): Result {\n        val lock = getLock(input1, input2, input3)\n        return synchronized(lock) {\n            values.getOrPut(Triple(input1, input2, input3)) { function(input1, input2, input3) }\n        }\n    }\n}\n\n/**\n * A class that memoizes the result of a function. This method is threadsafe. Only one thread will\n * ever invoke the backing function, other threads will block until a result is available, and then\n * return that result. The result will expire after the defined timeout, at which point the function\n * can be executed again.\n */\nprivate class MemoizeExpiring0<out Result>(\n    private val validFor: Duration,\n    private val function: () -> Result,\n) : () -> Result {\n    @Volatile private var value: Any? = UninitializedValue\n    private var expiration: ClockMark? = null\n\n    @Synchronized\n    override fun invoke(): Result {\n        if (value == UninitializedValue || expiration?.hasPassed() != false) {\n            value = function()\n            expiration = Clock.markNow() + validFor\n        }\n        @Suppress(\"UNCHECKED_CAST\") return (value as Result)\n    }\n}\n\n/**\n * A class that memoizes the result of a function. This method is threadsafe. Only one thread will\n * ever invoke the backing function, other threads will block until a result is available, and then\n * return that result. The result will expire after the defined timeout, at which point the function\n * can be executed again.\n */\nprivate class MemoizeExpiring1<in Input, out Result>(\n    private val validFor: Duration,\n    private val function: (Input) -> Result,\n) : (Input) -> Result {\n    private val values = mutableMapOf<Input, Pair<Result, ClockMark>>()\n    private val locks = mutableMapOf<Input, Any>()\n\n    @Synchronized\n    private fun getLock(input: Input): Any = locks.getOrPut(input) { Object() }\n\n    override fun invoke(input: Input): Result {\n        val lock = getLock(input)\n        return synchronized(lock) {\n            val (result, expiration) = values[input] ?: UninitializedValue to null\n            if (result == UninitializedValue || expiration?.hasPassed() != false) {\n                val computedResult = function(input)\n                values[input] = computedResult to Clock.markNow() + validFor\n                computedResult\n            } else {\n                @Suppress(\"UNCHECKED_CAST\") (result as Result)\n            }\n        }\n    }\n}\n\n/**\n * A class that memoizes the result of a function. This method is threadsafe. Only one thread will\n * ever invoke the backing function, other threads will block until a result is available, and then\n * return that result. The result will expire after the defined timeout, at which point the function\n * can be executed again.\n */\nprivate class MemoizeExpiring2<in Input1, in Input2, out Result>(\n    private val validFor: Duration,\n    private val function: (Input1, Input2) -> Result,\n) : (Input1, Input2) -> Result {\n    private val values = mutableMapOf<Pair<Input1, Input2>, Pair<Result, ClockMark>>()\n    private val locks = mutableMapOf<Pair<Input1, Input2>, Any>()\n\n    @Synchronized\n    private fun getLock(input1: Input1, input2: Input2): Any = locks.getOrPut(input1 to input2) { Object() }\n\n    override fun invoke(input1: Input1, input2: Input2): Result {\n        val lock = getLock(input1, input2)\n        return synchronized(lock) {\n            val (result, expiration) = values[input1 to input2] ?: UninitializedValue to null\n            if (result == UninitializedValue || expiration?.hasPassed() != false) {\n                val computedResult = function(input1, input2)\n                values[input1 to input2] = computedResult to Clock.markNow() + validFor\n                computedResult\n            } else {\n                @Suppress(\"UNCHECKED_CAST\") (result as Result)\n            }\n        }\n    }\n}\n\n/**\n * A class that memoizes the result of a function. This method is threadsafe. Only one thread will\n * ever invoke the backing function, other threads will block until a result is available, and then\n * return that result. The result will expire after the defined timeout, at which point the function\n * can be executed again.\n */\nprivate class MemoizeExpiring3<in Input1, in Input2, in Input3, out Result>(\n    private val validFor: Duration,\n    private val function: (Input1, Input2, Input3) -> Result,\n) : (Input1, Input2, Input3) -> Result {\n    private val values = mutableMapOf<Triple<Input1, Input2, Input3>, Pair<Result, ClockMark>>()\n    private val locks = mutableMapOf<Triple<Input1, Input2, Input3>, Any>()\n\n    @Synchronized\n    private fun getLock(input1: Input1, input2: Input2, input3: Input3): Any = locks.getOrPut(Triple(input1, input2, input3)) { Object() }\n\n    override fun invoke(input1: Input1, input2: Input2, input3: Input3): Result {\n        val lock = getLock(input1, input2, input3)\n        return synchronized(lock) {\n            val (result, expiration) = values[Triple(input1, input2, input3)] ?: UninitializedValue to null\n            if (result == UninitializedValue || expiration?.hasPassed() != false) {\n                val computedResult = function(input1, input2, input3)\n                values[Triple(input1, input2, input3)] = computedResult to Clock.markNow() + validFor\n                computedResult\n            } else {\n                @Suppress(\"UNCHECKED_CAST\") (result as Result)\n            }\n        }\n    }\n}\n\n/**\n * Cache the result from calling this method. Subsequent calls, even with different parameters, will\n * not change the cached output.\n *\n * TODO: use contracts when they're no longer experimental\n */\nprivate class CachedFirstResultSuspend1<in Input, out Result>(\n    private val f: suspend (Input) -> Result\n) {\n    // contract { callsInPlace(f, EXACTLY_ONCE) }\n    private val initializeMutex = Mutex()\n\n    private object UNINITIALIZED_VALUE\n    @Volatile private var value: Any? = UNINITIALIZED_VALUE\n\n    fun cacheFirstResult(): suspend (Input) -> Result = { input ->\n        initializeMutex.withLock {\n            if (value == UNINITIALIZED_VALUE) {\n                value = f(input)\n            }\n            @Suppress(\"UNCHECKED_CAST\") (value as Result)\n        }\n    }\n}\n\n/**\n * Cache the result from calling this method. Subsequent calls, even with different parameters, will\n * not change the cached output.\n *\n * TODO: use contracts when they're no longer experimental\n */\nprivate class CachedFirstResultSuspend2<in Input1, in Input2, out Result>(\n    private val f: suspend (Input1, Input2) -> Result\n) {\n    // contract { callsInPlace(f, EXACTLY_ONCE) }\n    private val initializeMutex = Mutex()\n\n    private object UNINITIALIZED_VALUE\n    @Volatile private var value: Any? = UNINITIALIZED_VALUE\n\n    fun cacheFirstResult(): suspend (Input1, Input2) -> Result = { input1, input2 ->\n        initializeMutex.withLock {\n            if (value == UNINITIALIZED_VALUE) {\n                value = f(input1, input2)\n            }\n            @Suppress(\"UNCHECKED_CAST\") (value as Result)\n        }\n    }\n}\n\n/**\n * Cache the result from calling this method. Subsequent calls, even with different parameters, will\n * not change the cached output.\n *\n * TODO: use contracts when they're no longer experimental\n */\nprivate class CachedFirstResultSuspend3<in Input1, in Input2, in Input3, out Result>(\n    private val f: suspend (Input1, Input2, Input3) -> Result\n) {\n    // contract { callsInPlace(f, EXACTLY_ONCE) }\n    private val initializeMutex = Mutex()\n\n    private object UNINITIALIZED_VALUE\n    @Volatile private var value: Any? = UNINITIALIZED_VALUE\n\n    fun cacheFirstResult(): suspend (Input1, Input2, Input3) -> Result = { input1, input2, input3 ->\n        initializeMutex.withLock {\n            if (value == UNINITIALIZED_VALUE) {\n                value = f(input1, input2, input3)\n            }\n            @Suppress(\"UNCHECKED_CAST\") (value as Result)\n        }\n    }\n}\n\n/**\n * Cache the result from calling this method. Subsequent calls, even with different parameters, will\n * not change the cached output.\n *\n * TODO: use contracts when they're no longer experimental\n */\nprivate class CachedFirstResult1<in Input, out Result>(\n    private val function: (Input) -> Result\n) : (Input) -> Result {\n    // contract { callsInPlace(f, EXACTLY_ONCE) }\n    private object UNINITIALIZED_VALUE\n    @Volatile private var value: Any? = UNINITIALIZED_VALUE\n\n    @Synchronized\n    override fun invoke(input: Input): Result {\n        if (value == UNINITIALIZED_VALUE) {\n            value = function(input)\n        }\n        @Suppress(\"UNCHECKED_CAST\") return (value as Result)\n    }\n}\n\n/**\n * Cache the result from calling this method. Subsequent calls, even with different parameters, will\n * not change the cached output.\n *\n * TODO: use contracts when they're no longer experimental\n */\nprivate class CachedFirstResult2<in Input1, in Input2, out Result>(\n    private val function: (Input1, Input2) -> Result\n) : (Input1, Input2) -> Result {\n    // contract { callsInPlace(f, EXACTLY_ONCE) }\n    private object UNINITIALIZED_VALUE\n    @Volatile private var value: Any? = UNINITIALIZED_VALUE\n\n    @Synchronized\n    override fun invoke(input1: Input1, input2: Input2): Result {\n        if (value == UNINITIALIZED_VALUE) {\n            value = function(input1, input2)\n        }\n        @Suppress(\"UNCHECKED_CAST\") return (value as Result)\n    }\n}\n\n/**\n * Cache the result from calling this method. Subsequent calls, even with different parameters, will\n * not change the cached output.\n *\n * TODO: use contracts when they're no longer experimental\n */\nprivate class CachedFirstResult3<in Input1, in Input2, in Input3, out Result>(\n    private val function: (Input1, Input2, Input3) -> Result\n) : (Input1, Input2, Input3) -> Result {\n    // contract { callsInPlace(f, EXACTLY_ONCE) }\n    private object UNINITIALIZED_VALUE\n    @Volatile private var value: Any? = UNINITIALIZED_VALUE\n\n    @Synchronized\n    override fun invoke(input1: Input1, input2: Input2, input3: Input3): Result {\n        if (value == UNINITIALIZED_VALUE) {\n            value = function(input1, input2, input3)\n        }\n        @Suppress(\"UNCHECKED_CAST\") return (value as Result)\n    }\n}\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Result> (() -> Result).memoized(): () -> Result = Memoize0(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input, Result> ((Input) -> Result).memoized(): (Input) -> Result = Memoize1(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Result> ((Input1, Input2) -> Result).memoized(): (Input1, Input2) -> Result = Memoize2(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Input3, Result> ((Input1, Input2, Input3) -> Result).memoized(): (Input1, Input2, Input3) -> Result = Memoize3(this)\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Result> (() -> Result).memoized(validFor: Duration): () -> Result = MemoizeExpiring0(validFor, this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input, Result> ((Input) -> Result).memoized(validFor: Duration): (Input) -> Result = MemoizeExpiring1(validFor, this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Result> ((Input1, Input2) -> Result).memoized(validFor: Duration): (Input1, Input2) -> Result = MemoizeExpiring2(validFor, this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Input3, Result> ((Input1, Input2, Input3) -> Result).memoized(validFor: Duration): (Input1, Input2, Input3) -> Result = MemoizeExpiring3(validFor, this)\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Result> (suspend () -> Result).memoizedSuspend() = MemoizeSuspend0(this).memoize()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input, Result> (suspend (Input) -> Result).memoizedSuspend() = MemoizeSuspend1(this).memoize()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Result> (suspend (Input1, Input2) -> Result).memoizedSuspend() = MemoizeSuspend2(this).memoize()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Input3, Result> (suspend (Input1, Input2, Input3) -> Result).memoizedSuspend() = MemoizeSuspend3(this).memoize()\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Result> (suspend () -> Result).memoizedSuspend(validFor: Duration) = MemoizeSuspendExpiring0(validFor, this).memoize()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input, Result> (suspend (Input) -> Result).memoizedSuspend(validFor: Duration) = MemoizeSuspendExpiring1(validFor, this).memoize()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Result> (suspend (Input1, Input2) -> Result).memoizedSuspend(validFor: Duration) = MemoizeSuspendExpiring2(validFor, this).memoize()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Input3, Result> (suspend (Input1, Input2, Input3) -> Result).memoizedSuspend(validFor: Duration) = MemoizeSuspendExpiring3(validFor, this).memoize()\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Result> memoize(f: () -> Result): () -> Result = Memoize0(f)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input, Result> memoize(f: (Input) -> Result): (Input) -> Result = Memoize1(f)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Result> memoize(f: (Input1, Input2) -> Result): (Input1, Input2) -> Result = Memoize2(f)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Input3, Result> memoize(f: (Input1, Input2, Input3) -> Result): (Input1, Input2, Input3) -> Result = Memoize3(f)\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Result> memoize(validFor: Duration, f: () -> Result): () -> Result = MemoizeExpiring0(validFor, f)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input, Result> memoize(validFor: Duration, f: (Input) -> Result): (Input) -> Result = MemoizeExpiring1(validFor, f)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Result> memoize(validFor: Duration, f: (Input1, Input2) -> Result): (Input1, Input2) -> Result = MemoizeExpiring2(validFor, f)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Input3, Result> memoize(validFor: Duration, f: (Input1, Input2, Input3) -> Result): (Input1, Input2, Input3) -> Result = MemoizeExpiring3(validFor, f)\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Result> memoizeSuspend(f: suspend() -> Result): suspend () -> Result = MemoizeSuspend0(f).memoize()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input, Result> memoizeSuspend(f: suspend(Input) -> Result): suspend (Input) -> Result = MemoizeSuspend1(f).memoize()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Result> memoizeSuspend(f: suspend(Input1, Input2) -> Result): suspend (Input1, Input2) -> Result = MemoizeSuspend2(f).memoize()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Input3, Result> memoizeSuspend(f: suspend(Input1, Input2, Input3) -> Result): suspend (Input1, Input2, Input3) -> Result = MemoizeSuspend3(f).memoize()\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Result> memoizeSuspend(validFor: Duration, f: suspend() -> Result): suspend () -> Result = MemoizeSuspendExpiring0(validFor, f).memoize()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input, Result> memoizeSuspend(validFor: Duration, f: suspend(Input) -> Result): suspend (Input) -> Result = MemoizeSuspendExpiring1(validFor, f).memoize()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Result> memoizeSuspend(validFor: Duration, f: suspend(Input1, Input2) -> Result): suspend (Input1, Input2) -> Result = MemoizeSuspendExpiring2(validFor, f).memoize()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Input3, Result> memoizeSuspend(validFor: Duration, f: suspend(Input1, Input2, Input3) -> Result): suspend (Input1, Input2, Input3) -> Result = MemoizeSuspendExpiring3(validFor, f).memoize()\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Result> (() -> Result).cachedFirstResult(): () -> Result = Memoize0(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input, Result> ((Input) -> Result).cachedFirstResult(): (Input) -> Result = CachedFirstResult1(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Result> ((Input1, Input2) -> Result).cachedFirstResult(): (Input1, Input2) -> Result = CachedFirstResult2(this)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Input3, Result> ((Input1, Input2, Input3) -> Result).cachedFirstResult(): (Input1, Input2, Input3) -> Result = CachedFirstResult3(this)\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Result> (suspend () -> Result).cachedFirstResultSuspend() = MemoizeSuspend0(this).memoize()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input, Result> (suspend (Input) -> Result).cachedFirstResultSuspend() = CachedFirstResultSuspend1(this).cacheFirstResult()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Result> (suspend (Input1, Input2) -> Result).cachedFirstResultSuspend() = CachedFirstResultSuspend2(this).cacheFirstResult()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Input3, Result> (suspend (Input1, Input2, Input3) -> Result).cachedFirstResultSuspend() = CachedFirstResultSuspend3(this).cacheFirstResult()\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Result> cacheFirstResult(f: () -> Result): () -> Result = Memoize0(f)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input, Result> cacheFirstResult(f: (Input) -> Result): (Input) -> Result = CachedFirstResult1(f)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Result> cacheFirstResult(f: (Input1, Input2) -> Result): (Input1, Input2) -> Result = CachedFirstResult2(f)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Input3, Result> cacheFirstResult(f: (Input1, Input2, Input3) -> Result): (Input1, Input2, Input3) -> Result = CachedFirstResult3(f)\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Result> cacheFirstResultSuspend(f: suspend() -> Result) = MemoizeSuspend0(f).memoize()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input, Result> cacheFirstResultSuspend(f: suspend(Input) -> Result) = CachedFirstResultSuspend1(f).cacheFirstResult()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Result> cacheFirstResultSuspend(f: suspend(Input1, Input2) -> Result) = CachedFirstResultSuspend2(f).cacheFirstResult()\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\")\n)\nfun <Input1, Input2, Input3, Result> cacheFirstResultSuspend(f: suspend(Input1, Input2, Input3) -> Result) = CachedFirstResultSuspend3(f).cacheFirstResult()\n"
  },
  {
    "path": "scan-framework/src/main/java/com/getbouncer/scan/framework/util/Retry.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport com.getbouncer.scan.framework.time.Duration\nimport kotlinx.coroutines.delay\n\nprivate const val DEFAULT_RETRIES = 3\n\n/**\n * Call a given [task]. If the task throws an exception not included in the [excluding] list, retry\n * the task up to [times], each time after a delay of [retryDelay].\n *\n * @param retryDelay the amount of time between a failed task and the next retry\n * @param times the number of times to retry the task\n * @param excluding a list of exceptions to fail immediately on\n * @param task the task to retry\n *\n * TODO: use contracts when they're no longer experimental\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nsuspend fun <T> retry(\n    retryDelay: Duration,\n    times: Int = DEFAULT_RETRIES,\n    excluding: List<Class<out Throwable>> = emptyList(),\n    task: suspend () -> T\n): T {\n//    contract { callsInPlace(task, InvocationKind.AT_LEAST_ONCE) }\n    var exception: Throwable? = null\n    for (attempt in 1..times) {\n        try {\n            return task()\n        } catch (t: Throwable) {\n            exception = t\n            if (t.javaClass in excluding) {\n                throw t\n            }\n            if (attempt < times) {\n                delay(retryDelay.inMilliseconds.toLong())\n            }\n        }\n    }\n\n    if (exception != null) {\n        throw exception\n    } else {\n        // This code should never be reached\n        throw UnexpectedRetryException()\n    }\n}\n\n/**\n * Call a given [task]. If the task throws an exception not included in the [excluding] list, retry\n * the task up to [times].\n *\n * @param times the number of times to retry the task\n * @param excluding a list of exceptions to fail immediately on\n * @param task the task to retry\n *\n * TODO: use contracts when they're no longer experimental\n */\n@Deprecated(message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\")\nfun <T> retrySync(\n    times: Int = DEFAULT_RETRIES,\n    excluding: List<Class<out Throwable>> = emptyList(),\n    task: () -> T\n): T {\n//    contract { callsInPlace(task, InvocationKind.AT_LEAST_ONCE) }\n    var exception: Throwable? = null\n    for (attempt in 1..times) {\n        try {\n            return task()\n        } catch (t: Throwable) {\n            exception = t\n            if (t.javaClass in excluding) {\n                throw t\n            }\n        }\n    }\n\n    if (exception != null) {\n        throw exception\n    } else {\n        // This code should never be reached\n        throw UnexpectedRetryException()\n    }\n}\n\n/**\n * This exception should never be thrown, and therefore can be private.\n */\nprivate class UnexpectedRetryException : Exception()\n"
  },
  {
    "path": "scan-framework/src/test/java/com/getbouncer/scan/framework/AnalyzerTest.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport androidx.test.filters.SmallTest\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runBlockingTest\nimport org.junit.Test\nimport kotlin.test.assertEquals\n\nclass AnalyzerTest {\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun analyzerPoolCreateNormally() = runBlockingTest {\n        class TestAnalyzerFactory : AnalyzerFactory<Int, Int, Int, TestAnalyzer> {\n            override suspend fun newInstance(): TestAnalyzer? = TestAnalyzer()\n        }\n\n        val analyzerPool = AnalyzerPool.of(\n            analyzerFactory = TestAnalyzerFactory(),\n            desiredAnalyzerCount = 12\n        )\n\n        assertEquals(12, analyzerPool.desiredAnalyzerCount)\n        assertEquals(12, analyzerPool.analyzers.size)\n        assertEquals(3, analyzerPool.analyzers[0].analyze(1, 2))\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun analyzerPoolCreateFailure() = runBlockingTest {\n        class TestAnalyzerFactory : AnalyzerFactory<Int, Int, Int, TestAnalyzer> {\n            override suspend fun newInstance(): TestAnalyzer? = null\n        }\n\n        val analyzerPool = AnalyzerPool.of(\n            analyzerFactory = TestAnalyzerFactory(),\n            desiredAnalyzerCount = 12\n        )\n\n        assertEquals(12, analyzerPool.desiredAnalyzerCount)\n        assertEquals(0, analyzerPool.analyzers.size)\n    }\n\n    private class TestAnalyzer : Analyzer<Int, Int, Int> {\n        override suspend fun analyze(data: Int, state: Int): Int = data + state\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/test/java/com/getbouncer/scan/framework/LoopTest.kt",
    "content": "package com.getbouncer.scan.framework\n\nimport androidx.test.filters.MediumTest\nimport androidx.test.filters.SmallTest\nimport com.getbouncer.scan.framework.time.Duration\nimport com.getbouncer.scan.framework.time.nanoseconds\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.test.runBlockingTest\nimport kotlinx.coroutines.yield\nimport org.junit.Ignore\nimport org.junit.Test\nimport java.util.concurrent.atomic.AtomicInteger\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertNull\nimport kotlin.test.assertTrue\nimport kotlin.test.fail\n\nclass LoopTest {\n\n    @Test(timeout = 1000)\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun processBoundAnalyzerLoop_analyzeData() = runBlockingTest {\n        val dataCount = 3\n        val resultCount = AtomicInteger(0)\n\n        class TestResultHandler : StatefulResultHandler<Int, Int, String, Boolean>(1) {\n            override suspend fun onResult(result: String, data: Int): Boolean {\n                assertEquals(1, state)\n                val count = resultCount.incrementAndGet()\n                return count >= dataCount\n            }\n        }\n\n        val analyzerPool = AnalyzerPool.of(\n            analyzerFactory = TestAnalyzerFactory(),\n            desiredAnalyzerCount = 12\n        )\n\n        val loop = ProcessBoundAnalyzerLoop(\n            analyzerPool = analyzerPool,\n            analyzerLoopErrorListener = object : AnalyzerLoopErrorListener {\n                override fun onAnalyzerFailure(t: Throwable): Boolean { fail(t.message) }\n                override fun onResultFailure(t: Throwable): Boolean { fail(t.message) }\n            },\n            resultHandler = TestResultHandler()\n        )\n\n        val channel = Channel<Int>(dataCount)\n        val job = loop.subscribeTo(channel.receiveAsFlow(), this)\n        assertNotNull(job)\n\n        repeat(dataCount) {\n            while (!channel.offer(it)) {\n                // loop until the channel accepts the data\n            }\n        }\n\n        job.joinTest()\n        assertTrue { dataCount == resultCount.get() }\n    }\n\n    @Test(timeout = 200)\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun processBoundAnalyzerLoop_analyzeDataNoDuplicates() = runBlockingTest {\n        val dataCount = 3\n        var resultCount = 0\n\n        val dataProcessMutex = Mutex()\n        val dataToProcess = mutableMapOf<Int, Boolean>()\n        repeat(dataCount) {\n            dataToProcess[it] = false\n        }\n\n        class TestResultHandler : StatefulResultHandler<Int, Int, String, Boolean>(1) {\n            override suspend fun onResult(result: String, data: Int): Boolean {\n                dataProcessMutex.withLock {\n                    resultCount++\n                    assertEquals(1, state)\n                    assertTrue { dataToProcess[data] == false }\n                    dataToProcess[data] = true\n                    return resultCount >= dataCount\n                }\n            }\n        }\n\n        val analyzerPool = AnalyzerPool.of(\n            analyzerFactory = TestAnalyzerFactory(),\n            desiredAnalyzerCount = 12\n        )\n\n        val loop = ProcessBoundAnalyzerLoop(\n            analyzerPool = analyzerPool,\n            analyzerLoopErrorListener = object : AnalyzerLoopErrorListener {\n                override fun onAnalyzerFailure(t: Throwable): Boolean { fail(t.message) }\n                override fun onResultFailure(t: Throwable): Boolean { fail(t.message) }\n            },\n            resultHandler = TestResultHandler()\n        )\n\n        val channel = Channel<Int>(dataCount)\n        val job = loop.subscribeTo(channel.receiveAsFlow(), this)\n        assertNotNull(job)\n\n        repeat(dataCount) {\n            while (!channel.offer(it)) {\n                // loop until the channel accepts the data\n            }\n        }\n\n        job.joinTest()\n        val dataProcessedCount = dataProcessMutex.withLock { resultCount }\n        assertTrue { dataCount == dataProcessedCount }\n\n        repeat(dataCount) {\n            assertTrue { dataToProcess[it] == true }\n        }\n    }\n\n    @Test(timeout = 200)\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun processBoundAnalyzerLoop_noAnalyzersAvailable() = runBlockingTest {\n        var analyzerFailure = false\n\n        class TestResultHandler : StatefulResultHandler<Int, Int, String, Boolean>(1) {\n            override suspend fun onResult(result: String, data: Int): Boolean {\n                assertEquals(1, state)\n                return false\n            }\n        }\n\n        val analyzerPool = AnalyzerPool.of(\n            analyzerFactory = TestAnalyzerFactory(),\n            desiredAnalyzerCount = 0\n        )\n\n        val loop = ProcessBoundAnalyzerLoop(\n            analyzerPool = analyzerPool,\n            analyzerLoopErrorListener = object : AnalyzerLoopErrorListener {\n                override fun onAnalyzerFailure(t: Throwable): Boolean { analyzerFailure = true; return true }\n                override fun onResultFailure(t: Throwable): Boolean { fail(t.message) }\n            },\n            resultHandler = TestResultHandler()\n        )\n\n        val channel = Channel<Int>(Channel.RENDEZVOUS)\n        val job = loop.subscribeTo(channel.receiveAsFlow(), this)\n        assertNull(job)\n        assertTrue { analyzerFailure }\n    }\n\n    @Test(timeout = 1000)\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    @Ignore(\"This test is flaking in CI\")\n    fun finiteAnalyzerLoop_analyzeData() = runBlockingTest {\n        val dataCount = 3\n        var dataProcessed = false\n        val resultCount = AtomicInteger(0)\n\n        class TestResultHandler : TerminatingResultHandler<Int, Int, String>(1) {\n            override suspend fun onResult(result: String, data: Int) {\n                assertEquals(1, state)\n                resultCount.incrementAndGet()\n            }\n\n            override suspend fun onAllDataProcessed() {\n                assertEquals(dataCount, resultCount.get())\n                dataProcessed = true\n            }\n\n            override suspend fun onTerminatedEarly() {\n                fail()\n            }\n        }\n\n        val analyzerPool = AnalyzerPool.of(\n            analyzerFactory = TestAnalyzerFactory(),\n            desiredAnalyzerCount = 12\n        )\n\n        val loop = FiniteAnalyzerLoop(\n            analyzerPool = analyzerPool,\n            analyzerLoopErrorListener = object : AnalyzerLoopErrorListener {\n                override fun onAnalyzerFailure(t: Throwable): Boolean { fail(t.message) }\n                override fun onResultFailure(t: Throwable): Boolean { fail(t.message) }\n            },\n            resultHandler = TestResultHandler(),\n            timeLimit = Duration.INFINITE\n        )\n\n        val job = loop.process((0 until dataCount).map { 2 }, this)\n        assertNotNull(job)\n        job.joinTest()\n\n        assertTrue(dataProcessed)\n    }\n\n    @Test(timeout = 1000)\n    @MediumTest\n    @ExperimentalCoroutinesApi\n    fun finiteAnalyzerLoop_analyzeDataTimeout() = runBlockingTest {\n        val dataCount = 10000\n        val resultCount = AtomicInteger(0)\n        var terminatedEarly = false\n\n        class TestResultHandler : TerminatingResultHandler<Int, Int, String>(1) {\n            override suspend fun onResult(result: String, data: Int) {\n                assertEquals(1, state)\n                resultCount.incrementAndGet()\n            }\n\n            override suspend fun onAllDataProcessed() {\n                fail()\n            }\n\n            override suspend fun onTerminatedEarly() {\n                assertTrue { resultCount.get() < dataCount }\n                terminatedEarly = true\n            }\n        }\n\n        val analyzerPool = AnalyzerPool.of(\n            analyzerFactory = TestAnalyzerFactory(),\n            desiredAnalyzerCount = 12\n        )\n\n        val loop = FiniteAnalyzerLoop(\n            analyzerPool = analyzerPool,\n            analyzerLoopErrorListener = object : AnalyzerLoopErrorListener {\n                override fun onAnalyzerFailure(t: Throwable): Boolean { fail(t.message) }\n                override fun onResultFailure(t: Throwable): Boolean { fail(t.message) }\n            },\n            resultHandler = TestResultHandler(),\n            timeLimit = 1.nanoseconds\n        )\n\n        val job = loop.process((0 until dataCount).map { 2 }, this)\n        assertNotNull(job)\n        job.joinTest()\n\n        assertTrue { terminatedEarly }\n    }\n\n    @Test(timeout = 200)\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun finiteAnalyzerLoop_analyzeDataNoData() = runBlockingTest {\n        var dataProcessed = false\n\n        class TestResultHandler : TerminatingResultHandler<Int, Int, String>(1) {\n            override suspend fun onResult(result: String, data: Int) { fail() }\n\n            override suspend fun onAllDataProcessed() { dataProcessed = true }\n\n            override suspend fun onTerminatedEarly() { fail() }\n        }\n\n        val analyzerPool = AnalyzerPool.of(\n            analyzerFactory = TestAnalyzerFactory(),\n            desiredAnalyzerCount = 12\n        )\n\n        val loop = FiniteAnalyzerLoop(\n            analyzerPool = analyzerPool,\n            analyzerLoopErrorListener = object : AnalyzerLoopErrorListener {\n                override fun onAnalyzerFailure(t: Throwable): Boolean { fail(t.message) }\n                override fun onResultFailure(t: Throwable): Boolean { fail(t.message) }\n            },\n            resultHandler = TestResultHandler(),\n            timeLimit = Duration.INFINITE\n        )\n\n        val job = loop.process(emptyList(), this)\n        assertNotNull(job)\n        job.joinTest()\n\n        assertTrue { dataProcessed }\n    }\n\n    private class TestAnalyzer : Analyzer<Int, Int, String> {\n        companion object {\n            private val analyzerCounter = AtomicInteger(0)\n        }\n\n        private val analyzerNumber = analyzerCounter.getAndIncrement()\n        override suspend fun analyze(data: Int, state: Int): String = \"Analyzer=$analyzerNumber, data=$data, state=$state\"\n    }\n\n    private class TestAnalyzerFactory : AnalyzerFactory<Int, Int, String, TestAnalyzer> {\n        override suspend fun newInstance(): TestAnalyzer? = TestAnalyzer()\n    }\n\n    private suspend fun Job.joinTest() {\n        while (!isCompleted) {\n            yield()\n        }\n        join()\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/test/java/com/getbouncer/scan/framework/interop/BlockingAnalyzerTest.java",
    "content": "package com.getbouncer.scan.framework.interop;\n\nimport androidx.test.filters.MediumTest;\n\nimport com.getbouncer.scan.framework.Analyzer;\nimport com.getbouncer.scan.framework.AnalyzerFactory;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\nimport org.junit.Assert;\nimport org.junit.Test;\n\nimport kotlin.coroutines.Continuation;\nimport kotlin.jvm.functions.Function2;\nimport kotlinx.coroutines.BuildersKt;\nimport kotlinx.coroutines.CoroutineScope;\nimport kotlinx.coroutines.CoroutineStart;\nimport kotlinx.coroutines.Deferred;\nimport kotlinx.coroutines.Dispatchers;\nimport kotlinx.coroutines.GlobalScope;\n\npublic class BlockingAnalyzerTest {\n\n    @Test(timeout = 1000)\n    @MediumTest\n    public void blockingAnalyzer_works() throws InterruptedException {\n        final Analyzer<Integer, Boolean, Boolean> analyzer = new BlockingAnalyzer<Integer, Boolean, Boolean>() {\n            @Override\n            public Boolean analyzeBlocking(Integer data, Boolean state) {\n                return data > 0 && state;\n            }\n        };\n\n        final Deferred<Boolean> deferred = BuildersKt.async(\n            GlobalScope.INSTANCE,\n            Dispatchers.getDefault(),\n            CoroutineStart.DEFAULT,\n            new Function2<CoroutineScope, Continuation<? super Boolean>, Object>() {\n                @Override\n                public Object invoke(CoroutineScope coroutineScope, Continuation<? super Boolean> continuation) {\n                    return analyzer.analyze(1, true, continuation);\n                }\n            }\n        );\n\n        while (!deferred.isCompleted()) {\n            Thread.sleep(100);\n        }\n\n        Assert.assertTrue(deferred.getCompleted());\n    }\n\n    @Test(timeout = 1000)\n    @MediumTest\n    public void blockingAnalyzerFactory_works() throws InterruptedException {\n        final AnalyzerFactory<Integer, Boolean, Boolean, Analyzer<Integer, Boolean, Boolean>> factory =\n            new BlockingAnalyzerFactory<Integer, Boolean, Boolean, Analyzer<Integer, Boolean, Boolean>>() {\n                @Override\n                public Analyzer<Integer, Boolean, Boolean> newInstanceBlocking() {\n                    return new Analyzer<Integer, Boolean, Boolean>() {\n                        @Nullable\n                        @Override\n                        public Object analyze(\n                            Integer data,\n                            Boolean state,\n                            @NotNull Continuation<? super Boolean> $completion\n                        ) {\n                            return null;\n                        }\n                    };\n                }\n            };\n\n        final Deferred<Analyzer<Integer, Boolean, Boolean>> deferred = BuildersKt.async(\n            GlobalScope.INSTANCE,\n            Dispatchers.getDefault(),\n            CoroutineStart.DEFAULT,\n            new Function2<CoroutineScope, Continuation<? super Analyzer<Integer, Boolean, Boolean>>, Object>() {\n                @Override\n                public Object invoke(\n                    CoroutineScope coroutineScope,\n                    Continuation<? super Analyzer<Integer, Boolean, Boolean>> continuation\n                ) {\n                    return factory.newInstance(continuation);\n                }\n            }\n        );\n\n        while (!deferred.isCompleted()) {\n            Thread.sleep(100);\n        }\n\n        Assert.assertNotNull(deferred.getCompleted());\n    }\n\n    @Test\n    @MediumTest\n    public void blockingAnalyzerPoolFactory_works() {\n        final AnalyzerFactory<Integer, Boolean, Boolean, Analyzer<Integer, Boolean, Boolean>> factory =\n            new BlockingAnalyzerFactory<Integer, Boolean, Boolean, Analyzer<Integer, Boolean, Boolean>>() {\n                @Override\n                public Analyzer<Integer, Boolean, Boolean> newInstanceBlocking() {\n                    return new Analyzer<Integer, Boolean, Boolean>() {\n                        @Nullable\n                        @Override\n                        public Object analyze(\n                            Integer data,\n                            Boolean state,\n                            @NotNull Continuation<? super Boolean> $completion\n                        ) {\n                            return null;\n                        }\n                    };\n                }\n            };\n\n        final BlockingAnalyzerPoolFactory<Integer, Boolean, Boolean> poolFactory =\n                new BlockingAnalyzerPoolFactory<>(factory);\n\n        Assert.assertNotNull(poolFactory.buildAnalyzerPool());\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/test/java/com/getbouncer/scan/framework/interop/BlockingResultTest.java",
    "content": "package com.getbouncer.scan.framework.interop;\n\nimport androidx.test.filters.MediumTest;\n\nimport com.getbouncer.scan.framework.AggregateResultListener;\nimport com.getbouncer.scan.framework.ResultAggregator;\nimport com.getbouncer.scan.framework.ResultHandler;\nimport com.getbouncer.scan.framework.StatefulResultHandler;\nimport com.getbouncer.scan.framework.TerminatingResultHandler;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.junit.Test;\n\nimport kotlin.Pair;\nimport kotlin.Unit;\nimport kotlin.coroutines.Continuation;\nimport kotlin.jvm.functions.Function2;\nimport kotlinx.coroutines.BuildersKt;\nimport kotlinx.coroutines.CoroutineScope;\nimport kotlinx.coroutines.CoroutineStart;\nimport kotlinx.coroutines.Deferred;\nimport kotlinx.coroutines.Dispatchers;\nimport kotlinx.coroutines.GlobalScope;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertTrue;\n\npublic class BlockingResultTest {\n\n    private static class TerminatingTestResult {\n        public boolean handledResult = false;\n        public boolean handledAllResults = false;\n        public boolean terminatedEarly = false;\n    }\n\n    private static class AggregateTestResult {\n        public boolean handledInterim = false;\n        public boolean handledFinal = false;\n        public boolean handledReset = false;\n    }\n\n    @Test(timeout = 1000)\n    @MediumTest\n    public void blockingResultHandler_works() throws InterruptedException {\n        final ResultHandler<Integer, Integer, Boolean> resultHandler =\n            new BlockingResultHandler<Integer, Integer, Boolean>() {\n                @Override\n                public Boolean onResultBlocking(Integer result, Integer data) {\n                    return result != null && result.equals(data);\n                }\n            };\n\n        final Deferred<Boolean> deferred = BuildersKt.async(\n            GlobalScope.INSTANCE,\n            Dispatchers.getDefault(),\n            CoroutineStart.DEFAULT,\n            new Function2<CoroutineScope, Continuation<? super Boolean>, Object>() {\n                @Override\n                public Object invoke(CoroutineScope coroutineScope, Continuation<? super Boolean> continuation) {\n                    return resultHandler.onResult(2, 2, continuation);\n                }\n            }\n        );\n\n        while (!deferred.isCompleted()) {\n            Thread.sleep(100);\n        }\n\n        assertTrue(deferred.getCompleted());\n    }\n\n    @Test(timeout = 1000)\n    @MediumTest\n    public void blockingStatefulResultHandler_works() throws InterruptedException {\n        final StatefulResultHandler<Integer, Boolean, Integer, Boolean> resultHandler =\n            new BlockingStatefulResultHandler<Integer, Boolean, Integer, Boolean>(true) {\n                @Override\n                public Boolean onResultBlocking(Integer result, Integer data) {\n                    return result != null && result.equals(data) && getState();\n                }\n            };\n\n        final Deferred<Boolean> deferred = BuildersKt.async(\n            GlobalScope.INSTANCE,\n            Dispatchers.getDefault(),\n            CoroutineStart.DEFAULT,\n            new Function2<CoroutineScope, Continuation<? super Boolean>, Object>() {\n                @Override\n                public Object invoke(CoroutineScope coroutineScope, Continuation<? super Boolean> continuation) {\n                    return resultHandler.onResult(2, 2, continuation);\n                }\n            }\n        );\n\n        while (!deferred.isCompleted()) {\n            Thread.sleep(100);\n        }\n\n        assertTrue(deferred.getCompleted());\n    }\n\n    @Test(timeout = 1000)\n    @MediumTest\n    public void blockingTerminatingResultHandler_works() throws InterruptedException {\n        final TerminatingTestResult testResult = new TerminatingTestResult();\n\n        final TerminatingResultHandler<Integer, Boolean, Integer> resultHandler =\n            new BlockingTerminatingResultHandler<Integer, Boolean, Integer>(true) {\n                @Override\n                public void onAllDataProcessedBlocking() {\n                    testResult.handledAllResults = true;\n                }\n\n                @Override\n                public void onTerminatedEarlyBlocking() {\n                    testResult.terminatedEarly = true;\n                }\n\n                @Override\n                public void onResultBlocking(Integer result, Integer data) {\n                    testResult.handledResult = true;\n                }\n            };\n\n        final Deferred<Unit> ranAllDataProcessed = BuildersKt.async(\n            GlobalScope.INSTANCE,\n            Dispatchers.getDefault(),\n            CoroutineStart.DEFAULT,\n            new Function2<CoroutineScope, Continuation<? super Unit>, Object>() {\n                @Override\n                public Object invoke(CoroutineScope coroutineScope, Continuation<? super Unit> continuation) {\n                    return resultHandler.onAllDataProcessed(continuation);\n                }\n            }\n        );\n\n        final Deferred<Unit> ranTerminatedEarly = BuildersKt.async(\n            GlobalScope.INSTANCE,\n            Dispatchers.getDefault(),\n            CoroutineStart.DEFAULT,\n            new Function2<CoroutineScope, Continuation<? super Unit>, Object>() {\n                @Override\n                public Object invoke(CoroutineScope coroutineScope, Continuation<? super Unit> continuation) {\n                    return resultHandler.onTerminatedEarly(continuation);\n                }\n            }\n        );\n\n        final Deferred<Unit> ranResult = BuildersKt.async(\n            GlobalScope.INSTANCE,\n            Dispatchers.getDefault(),\n            CoroutineStart.DEFAULT,\n            new Function2<CoroutineScope, Continuation<? super Unit>, Object>() {\n                @Override\n                public Object invoke(CoroutineScope coroutineScope, Continuation<? super Unit> continuation) {\n                    return resultHandler.onResult(2, 2, continuation);\n                }\n            }\n        );\n\n        while (!testResult.handledResult ||\n            !testResult.handledAllResults ||\n            !testResult.terminatedEarly ||\n            !ranAllDataProcessed.isCompleted() ||\n            !ranTerminatedEarly.isCompleted() ||\n            !ranResult.isCompleted()\n        ) {\n            Thread.sleep(100);\n        }\n    }\n\n    @Test(timeout = 1000L)\n    @MediumTest\n    public void blockingAggregateResultListener_works() throws InterruptedException {\n        final AggregateTestResult testResult = new AggregateTestResult();\n\n        final AggregateResultListener<Integer, Boolean> resultListener =\n            new BlockingAggregateResultListener<Integer, Boolean>() {\n                @Override\n                public void onInterimResultBlocking(Integer result) {\n                    testResult.handledInterim = true;\n                }\n\n                @Override\n                public void onResultBlocking(Boolean result) {\n                    testResult.handledFinal = true;\n                }\n\n                @Override\n                public void onResetBlocking() {\n                    testResult.handledReset = true;\n                }\n            };\n\n        final Deferred<Unit> deferredInterim = BuildersKt.async(\n            GlobalScope.INSTANCE,\n            Dispatchers.getDefault(),\n            CoroutineStart.DEFAULT,\n            new Function2<CoroutineScope, Continuation<? super Unit>, Object>() {\n                @Override\n                public Object invoke(CoroutineScope coroutineScope, Continuation<? super Unit> continuation) {\n                    return resultListener.onInterimResult(1, continuation);\n                }\n            }\n        );\n\n        final Deferred<Unit> deferredResult = BuildersKt.async(\n            GlobalScope.INSTANCE,\n            Dispatchers.getDefault(),\n            CoroutineStart.DEFAULT,\n            new Function2<CoroutineScope, Continuation<? super Unit>, Object>() {\n                @Override\n                public Object invoke(CoroutineScope coroutineScope, Continuation<? super Unit> continuation) {\n                    return resultListener.onResult(true, continuation);\n                }\n            }\n        );\n\n        final Deferred<Unit> deferredReset = BuildersKt.async(\n            GlobalScope.INSTANCE,\n            Dispatchers.getDefault(),\n            CoroutineStart.DEFAULT,\n            new Function2<CoroutineScope, Continuation<? super Unit>, Object>() {\n                @Override\n                public Object invoke(CoroutineScope coroutineScope, Continuation<? super Unit> continuation) {\n                    return resultListener.onReset(continuation);\n                }\n            }\n        );\n\n        while (!testResult.handledInterim ||\n            !testResult.handledFinal ||\n            !testResult.handledReset ||\n            !deferredInterim.isCompleted() ||\n            !deferredResult.isCompleted() ||\n            !deferredReset.isCompleted()\n        ) {\n            Thread.sleep(100);\n        }\n    }\n\n    @Test(timeout = 1000L)\n    @MediumTest\n    public void blockingResultAggregator_works() throws InterruptedException {\n        final AggregateResultListener<Integer, Boolean> listener =\n            new BlockingAggregateResultListener<Integer, Boolean>() {\n                @Override\n                public void onInterimResultBlocking(Integer result) { }\n\n                @Override\n                public void onResultBlocking(Boolean result) { }\n\n                @Override\n                public void onResetBlocking() { }\n            };\n\n        final ResultAggregator<Integer, Boolean, Integer, Integer, Boolean> resultAggregator =\n            new BlockingResultAggregator<Integer, Boolean, Integer, Integer, Boolean>(listener, true) {\n                @NotNull\n                @Override\n                public Pair<Integer, Boolean> aggregateResultBlocking(Integer frame, Integer result) {\n                    return new Pair<>(1, true);\n                }\n            };\n\n        final Deferred<Pair<? extends Integer, ? extends Boolean>> deferred = BuildersKt.async(\n            GlobalScope.INSTANCE,\n            Dispatchers.getDefault(),\n            CoroutineStart.DEFAULT,\n            new Function2<CoroutineScope, Continuation<? super Pair<? extends Integer, ? extends Boolean>>, Object>() {\n                @Override\n                public Object invoke(CoroutineScope coroutineScope, Continuation<? super Pair<? extends Integer, ? extends Boolean>> continuation) {\n                    return resultAggregator.aggregateResult(1, 1, continuation);\n                }\n            }\n        );\n\n        while (!deferred.isCompleted()) {\n            Thread.sleep(100);\n        }\n\n        assertEquals(1, (int) deferred.getCompleted().getFirst());\n        assertTrue(deferred.getCompleted().getSecond());\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/test/java/com/getbouncer/scan/framework/time/DurationTest.kt",
    "content": "package com.getbouncer.scan.framework.time\n\nimport androidx.test.filters.SmallTest\nimport org.junit.Test\nimport kotlin.math.pow\nimport kotlin.math.roundToInt\nimport kotlin.math.roundToLong\nimport kotlin.random.Random\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFalse\nimport kotlin.test.assertTrue\n\nprivate fun Float.truncate(digits: Int) = ((this * 10.0.pow(digits)).roundToLong() / 10.0.pow(digits)).toFloat()\nprivate fun Double.truncate(digits: Int) = (this * 10.0.pow(digits)).roundToLong() / 10.0.pow(digits)\n\nclass DurationTest {\n\n    @Test\n    @SmallTest\n    fun reflective() {\n        val randomInt = Random.nextInt(-10, 10)\n        val randomLong = Random.nextLong(-10, 10)\n        val randomFloat = Random.nextFloat() * 20 - 10\n        val randomDouble = Random.nextDouble() * 20 - 10\n\n        assertEquals(randomInt, randomInt.nanoseconds.inNanoseconds.toInt())\n        assertEquals(randomInt, randomInt.microseconds.inMicroseconds.roundToInt())\n        assertEquals(randomInt, randomInt.milliseconds.inMilliseconds.roundToInt())\n        assertEquals(randomInt, randomInt.seconds.inSeconds.roundToInt())\n        assertEquals(randomInt, randomInt.minutes.inMinutes.roundToInt())\n        assertEquals(randomInt, randomInt.hours.inHours.roundToInt())\n        assertEquals(randomInt, randomInt.days.inDays.roundToInt())\n        assertEquals(randomInt, randomInt.weeks.inWeeks.roundToInt())\n        assertEquals(randomInt, randomInt.months.inMonths.roundToInt())\n        assertEquals(randomInt, randomInt.years.inYears.roundToInt())\n\n        assertEquals(randomLong, randomLong.nanoseconds.inNanoseconds)\n        assertEquals(randomLong, randomLong.microseconds.inMicroseconds.roundToLong())\n        assertEquals(randomLong, randomLong.milliseconds.inMilliseconds.roundToLong())\n        assertEquals(randomLong, randomLong.seconds.inSeconds.roundToLong())\n        assertEquals(randomLong, randomLong.minutes.inMinutes.roundToLong())\n        assertEquals(randomLong, randomLong.hours.inHours.roundToLong())\n        assertEquals(randomLong, randomLong.days.inDays.roundToLong())\n        assertEquals(randomLong, randomLong.weeks.inWeeks.roundToLong())\n        assertEquals(randomLong, randomLong.months.inMonths.roundToLong())\n        assertEquals(randomLong, randomLong.years.inYears.roundToLong())\n\n        // These have to be truncated since the limiting factor for accuracy is nanoseconds.\n        assertEquals(randomFloat.roundToLong(), randomFloat.nanoseconds.inNanoseconds, randomFloat.toString())\n        assertEquals(randomFloat.truncate(3), randomFloat.microseconds.inMicroseconds.toFloat(), randomFloat.toString())\n        assertEquals(randomFloat.truncate(6), randomFloat.milliseconds.inMilliseconds.toFloat(), randomFloat.toString())\n        assertEquals(randomFloat.truncate(9), randomFloat.seconds.inSeconds.toFloat(), randomFloat.toString())\n        assertEquals(randomFloat, randomFloat.minutes.inMinutes.toFloat(), randomFloat.toString())\n        assertEquals(randomFloat, randomFloat.hours.inHours.toFloat(), randomFloat.toString())\n        assertEquals(randomFloat, randomFloat.days.inDays.toFloat(), randomFloat.toString())\n        assertEquals(randomFloat, randomFloat.weeks.inWeeks.toFloat(), randomFloat.toString())\n        assertEquals(randomFloat, randomFloat.months.inMonths.toFloat(), randomFloat.toString())\n        assertEquals(randomFloat, randomFloat.years.inYears.toFloat(), randomFloat.toString())\n\n        // These have to be truncated since the limiting factor for accuracy is nanoseconds.\n        assertEquals(randomDouble.roundToLong(), randomDouble.nanoseconds.inNanoseconds, randomDouble.toString())\n        assertEquals(randomDouble.truncate(3), randomDouble.microseconds.inMicroseconds, randomDouble.toString())\n        assertEquals(randomDouble.truncate(6), randomDouble.milliseconds.inMilliseconds.truncate(6), randomDouble.toString())\n        assertEquals(randomDouble.truncate(9), randomDouble.seconds.inSeconds.truncate(9), randomDouble.toString())\n        assertEquals(randomDouble.truncate(8), randomDouble.minutes.inMinutes.truncate(8), randomDouble.toString())\n        assertEquals(randomDouble.truncate(10), randomDouble.hours.inHours.truncate(10), randomDouble.toString())\n        assertEquals(randomDouble.truncate(10), randomDouble.days.inDays.truncate(10), randomDouble.toString())\n        assertEquals(randomDouble.truncate(11), randomDouble.weeks.inWeeks.truncate(11), randomDouble.toString())\n        assertEquals(randomDouble.truncate(11), randomDouble.months.inMonths.truncate(11), randomDouble.toString())\n        assertEquals(randomDouble.truncate(11), randomDouble.years.inYears.truncate(11), randomDouble.toString())\n    }\n\n    @Test\n    @SmallTest\n    fun minMax() {\n        assertEquals(5.seconds, max(1.milliseconds, 5.seconds))\n        assertEquals(5.seconds, max(5.seconds, 1.milliseconds))\n\n        assertEquals(1.milliseconds, min(1.milliseconds, 5.seconds))\n        assertEquals(1.milliseconds, min(5.seconds, 1.milliseconds))\n\n        assertEquals(Duration.INFINITE, max(1.milliseconds, Duration.INFINITE))\n        assertEquals(Duration.INFINITE, max(Duration.INFINITE, 1.milliseconds))\n        assertEquals(Duration.INFINITE, max(Duration.INFINITE, Duration.INFINITE))\n        assertEquals(Duration.ZERO, max(Duration.NEGATIVE_INFINITE, Duration.ZERO))\n        assertEquals(Duration.ZERO, max(Duration.ZERO, Duration.NEGATIVE_INFINITE))\n        assertEquals(Duration.NEGATIVE_INFINITE, max(Duration.NEGATIVE_INFINITE, Duration.NEGATIVE_INFINITE))\n\n        assertEquals(Duration.NEGATIVE_INFINITE, min(1.milliseconds, Duration.NEGATIVE_INFINITE))\n        assertEquals(Duration.NEGATIVE_INFINITE, min(Duration.NEGATIVE_INFINITE, 1.milliseconds))\n        assertEquals(Duration.NEGATIVE_INFINITE, min(Duration.NEGATIVE_INFINITE, Duration.NEGATIVE_INFINITE))\n        assertEquals(Duration.ZERO, min(Duration.ZERO, Duration.INFINITE))\n        assertEquals(Duration.ZERO, min(Duration.INFINITE, Duration.ZERO))\n        assertEquals(Duration.INFINITE, min(Duration.INFINITE, Duration.INFINITE))\n    }\n\n    @Test\n    @SmallTest\n    fun arithmetic() {\n        assertEquals(5005.milliseconds, 5.milliseconds + 5.seconds)\n        assertEquals(26.seconds, 6000.milliseconds + 20.seconds)\n\n        assertEquals(4995.milliseconds, 5.seconds - 5.milliseconds)\n        assertEquals(14.seconds, 20.seconds - 6000.milliseconds)\n        assertEquals((-5).seconds, 55.seconds - 1.minutes)\n\n        assertEquals(6.seconds, 2.seconds * 3)\n        assertEquals(1.weeks, 3.5.days * 2)\n        assertEquals(9.seconds, 6.seconds * 1.5F)\n        assertEquals(9.seconds, 6.seconds * 1.5)\n\n        assertEquals(2.days, (4.0 / 7).weeks / 2)\n        assertEquals(6.seconds, 12.seconds / 2)\n        assertEquals(4.minutes, 10.minutes / 2.5F)\n        assertEquals(4.minutes, 10.minutes / 2.5)\n\n        assertEquals((-5).years, -(5.years))\n        assertEquals(2.months, -((-2).months))\n    }\n\n    @Test\n    @SmallTest\n    fun comparison() {\n        assertTrue { 5.milliseconds > 5.microseconds }\n        assertFalse { 5.milliseconds < 5.microseconds }\n        assertTrue { 5.milliseconds >= 5.microseconds }\n\n        assertTrue { 5.milliseconds < 6.milliseconds }\n        assertFalse { 5.milliseconds > 6.milliseconds }\n        assertTrue { 5.milliseconds <= 6.milliseconds }\n\n        assertTrue { 2.hours == 120.minutes }\n        assertTrue { 2.hours >= 120.minutes }\n        assertTrue { 2.hours <= 120.minutes }\n    }\n\n    @Test\n    @SmallTest\n    fun absolutes() {\n        val duration = 2.years\n\n        assertEquals((2 * 365.25 * 24 * 60 * 60 * 1000 * 1000 * 1000).toLong(), duration.inNanoseconds)\n        assertEquals(2 * 365.25 * 24 * 60 * 60 * 1000 * 1000, duration.inMicroseconds)\n        assertEquals(2 * 365.25 * 24 * 60 * 60 * 1000, duration.inMilliseconds)\n        assertEquals(2 * 365.25 * 24 * 60 * 60, duration.inSeconds)\n        assertEquals(2 * 365.25 * 24 * 60, duration.inMinutes)\n        assertEquals(2 * 365.25 * 24, duration.inHours)\n        assertEquals(2 * 365.25, duration.inDays)\n        assertEquals(2 * 365.25 / 7, duration.inWeeks)\n        assertEquals(2 * 12.0, duration.inMonths)\n        assertEquals(2.0, duration.inYears)\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/test/java/com/getbouncer/scan/framework/util/ArrayExtensionsTest.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport androidx.test.filters.SmallTest\nimport org.junit.Assert\nimport org.junit.Test\nimport kotlin.random.Random\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNull\nimport kotlin.test.assertTrue\n\nclass ArrayExtensionsTest {\n\n    @Test\n    @SmallTest\n    fun reshape() {\n        val matrix = generateTestMatrix(1_000, 10_000)\n        val reshaped = matrix.reshape(100)\n\n        assertEquals(100_000, reshaped.size)\n        assertEquals(100, reshaped[0].size)\n        assertEquals(matrix[3][5], reshaped[30][5])\n    }\n\n    @Test\n    @SmallTest\n    fun updateEach_array() {\n        val array = (0 until 1_000_000).toList().toTypedArray()\n        array.updateEach { it + 1 }\n\n        Assert.assertArrayEquals((1..1_000_000).toList().toTypedArray(), array)\n    }\n\n    @Test\n    @SmallTest\n    fun updateEach_floatArray() {\n        val array = generateTestFloatArray(1_000_000)\n        val originalValue = array[100]\n        array.updateEach { it + 0.1F }\n\n        assertEquals(originalValue + 0.1F, array[100])\n    }\n\n    @Test\n    @SmallTest\n    fun filterByIndexes_FloatArray() {\n        val array = generateTestFloatArray(1_000_000)\n        val selectedIndexes = arrayOf(100).toIntArray() + IntArray(10_000) { Random.nextInt(array.size) }\n        val originalValue = array[100]\n\n        val filteredArray = array.filterByIndexes(selectedIndexes)\n\n        assertEquals(originalValue, filteredArray[0])\n    }\n\n    @Test\n    @SmallTest\n    fun filterByIndexes_TypedArray() {\n        val array = generateTestArray(1_000_000) { Random.nextInt() }\n        val selectedIndexes = arrayOf(100).toIntArray() + IntArray(10_000) { Random.nextInt(array.size) }\n        val originalValue = array[100]\n\n        val filteredArray = array.filterByIndexes(selectedIndexes)\n\n        assertEquals(originalValue, filteredArray[0])\n    }\n\n    @Test\n    @SmallTest\n    fun flatten() {\n        val matrix = generateTestMatrix(1_000, 10_000)\n        val flattened = matrix.flatten()\n\n        assertEquals(matrix[3][5], flattened[3005])\n    }\n\n    @Test\n    @SmallTest\n    fun transpose() {\n        val matrix = generateTestMatrix(1_000, 10_000)\n        val transposed = matrix.transpose()\n\n        assertEquals(matrix[1][1], transposed[1][1])\n        assertEquals(matrix[3][5], transposed[5][3])\n    }\n\n    @Test\n    @SmallTest\n    fun filteredIndexes() {\n        val array = generateTestFloatArray(10_000)\n        val filteredIndexes = array.filteredIndexes { it < 0.5F }\n\n        assertTrue { array.any { it >= 0.5F } }\n        assertTrue { filteredIndexes.map { array[it] }.all { it < 0.5F } }\n    }\n\n    @Test\n    @SmallTest\n    fun indexOfMax() {\n        val array = generateTestFloatArray(10_000)\n        val maxIndex = array.mapIndexed { index, value -> Pair(index, value) }.maxByOrNull { it.second }?.first\n\n        assertEquals(maxIndex, array.indexOfMax())\n        assertNull(FloatArray(0).indexOfMax())\n    }\n\n    /**\n     * Generate a test matrix.\n     */\n    private fun generateTestMatrix(width: Int, height: Int) =\n        Array(height) { generateTestFloatArray(width) }\n\n    /**\n     * Generate a test FloatArray.\n     */\n    private fun generateTestFloatArray(length: Int) = FloatArray(length) { Random.nextFloat() }\n\n    /**\n     * Generate a test array.\n     */\n    private inline fun <reified T> generateTestArray(length: Int, noinline generator: (Int) -> T) = Array(length, generator)\n}\n"
  },
  {
    "path": "scan-framework/src/test/java/com/getbouncer/scan/framework/util/FrameSaverTest.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runBlockingTest\nimport org.junit.Test\nimport kotlin.test.assertEquals\n\nclass FrameSaverTest {\n\n    private class TestFrameSaver : FrameSaver<String, Int, Int>() {\n        override fun getMaxSavedFrames(savedFrameIdentifier: String): Int = 3\n\n        override fun getSaveFrameIdentifier(frame: Int, metaData: Int): String? = when (metaData) {\n            1 -> \"one\"\n            2 -> \"two\"\n            else -> \"else\"\n        }\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun saveFrames() = runBlockingTest {\n        val frameSaver = TestFrameSaver()\n\n        frameSaver.saveFrame(1, 1)\n        frameSaver.saveFrame(1, 1)\n        frameSaver.saveFrame(1, 1)\n        frameSaver.saveFrame(1, 1)\n        frameSaver.saveFrame(2, 2)\n        frameSaver.saveFrame(3, 3)\n        frameSaver.saveFrame(4, 4)\n\n        assertEquals(\n            listOf(1, 1, 1),\n            frameSaver.getSavedFrames()[\"one\"]?.toList()\n        )\n\n        assertEquals(\n            listOf(2),\n            frameSaver.getSavedFrames()[\"two\"]?.toList()\n        )\n\n        assertEquals(\n            listOf(4, 3),\n            frameSaver.getSavedFrames()[\"else\"]?.toList()\n        )\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun doesNotLeakInternalMap() = runBlockingTest {\n        val frameSaver = TestFrameSaver()\n\n        frameSaver.saveFrame(1, 1)\n\n        val savedFrames = frameSaver.getSavedFrames()\n\n        frameSaver.reset()\n\n        assertEquals(1, savedFrames[\"one\"]?.first)\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/test/java/com/getbouncer/scan/framework/util/ItemCounterTest.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runBlockingTest\nimport org.junit.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNull\n\nclass ItemCounterTest {\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun countTotalItems() = runBlockingTest {\n        val itemCounter = ItemTotalCounter<String>()\n\n        assertEquals(1, itemCounter.countItem(\"a\"))\n        assertEquals(1, itemCounter.countItem(\"b\"))\n        assertEquals(2, itemCounter.countItem(\"a\"))\n\n        assertEquals(\"a\", itemCounter.getHighestCountItem()?.second)\n        assertEquals(2, itemCounter.getHighestCountItem()?.first)\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun resetTotal() = runBlockingTest {\n        val itemCounter = ItemTotalCounter<String>()\n\n        assertEquals(1, itemCounter.countItem(\"a\"))\n        assertEquals(1, itemCounter.countItem(\"b\"))\n        assertEquals(2, itemCounter.countItem(\"a\"))\n\n        itemCounter.reset()\n\n        assertNull(itemCounter.getHighestCountItem())\n\n        assertEquals(1, itemCounter.countItem(\"a\"))\n        assertEquals(1, itemCounter.countItem(\"b\"))\n        assertEquals(2, itemCounter.countItem(\"a\"))\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun countMinTotalItems() = runBlockingTest {\n        val itemCounter = ItemTotalCounter<String>()\n\n        assertEquals(1, itemCounter.countItem(\"a\"))\n        assertEquals(1, itemCounter.countItem(\"b\"))\n        assertEquals(2, itemCounter.countItem(\"a\"))\n\n        assertNull(itemCounter.getHighestCountItem(minCount = 3))\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun countRecentItems() = runBlockingTest {\n        val itemCounter = ItemRecencyCounter<String>(3)\n\n        assertEquals(1, itemCounter.countItem(\"a\")) // counter = [a]\n        assertEquals(1, itemCounter.countItem(\"b\")) // counter = [b, a]\n        assertEquals(2, itemCounter.countItem(\"a\")) // counter = [a, b, a]\n        assertEquals(2, itemCounter.countItem(\"a\")) // counter = [a, a, b]\n        assertEquals(3, itemCounter.countItem(\"a\")) // counter = [a, a, a]\n        assertEquals(3, itemCounter.countItem(\"a\")) // counter = [a, a, a]\n        assertEquals(1, itemCounter.countItem(\"b\")) // counter = [b, a, a]\n        assertEquals(2, itemCounter.countItem(\"a\")) // counter = [a, b, a]\n\n        assertEquals(\"a\", itemCounter.getHighestCountItem()?.second)\n        assertEquals(2, itemCounter.getHighestCountItem()?.first)\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun resetRecentItems() = runBlockingTest {\n        val itemCounter = ItemRecencyCounter<String>(3)\n\n        assertEquals(1, itemCounter.countItem(\"a\")) // counter = [a]\n        assertEquals(1, itemCounter.countItem(\"b\")) // counter = [b, a]\n        assertEquals(2, itemCounter.countItem(\"a\")) // counter = [a, b, a]\n\n        itemCounter.reset()\n\n        assertNull(itemCounter.getHighestCountItem())\n\n        assertEquals(1, itemCounter.countItem(\"a\")) // counter = [a]\n        assertEquals(1, itemCounter.countItem(\"b\")) // counter = [b, a]\n        assertEquals(2, itemCounter.countItem(\"a\")) // counter = [a, b, a]\n\n        assertEquals(\"a\", itemCounter.getHighestCountItem()?.second)\n        assertEquals(2, itemCounter.getHighestCountItem()?.first)\n    }\n\n    @Test\n    @ExperimentalCoroutinesApi\n    fun countMinRecentItems() = runBlockingTest {\n        val itemCounter = ItemRecencyCounter<String>(3)\n\n        assertEquals(1, itemCounter.countItem(\"a\"))\n        assertEquals(1, itemCounter.countItem(\"b\"))\n        assertEquals(2, itemCounter.countItem(\"a\"))\n\n        assertNull(itemCounter.getHighestCountItem(minCount = 3))\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/test/java/com/getbouncer/scan/framework/util/MemoizeTest.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport androidx.test.filters.SmallTest\nimport com.getbouncer.scan.framework.time.delay\nimport com.getbouncer.scan.framework.time.milliseconds\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.test.runBlockingTest\nimport org.junit.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertTrue\n\nclass MemoizeTest {\n\n    @Test\n    @SmallTest\n    fun memoize0wrapper_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = memoize<Boolean> {\n            functionRunCount++\n            true\n        }\n\n        assertTrue { testFunction.invoke() }\n        assertTrue { testFunction.invoke() }\n        assertTrue { testFunction.invoke() }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeExpiring0wrapper_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = memoize<Boolean>(50.milliseconds) {\n            functionRunCount++\n            true\n        }\n\n        assertTrue { testFunction.invoke() }\n        assertTrue { testFunction.invoke() }\n        assertTrue { testFunction.invoke() }\n\n        assertEquals(1, functionRunCount)\n\n        Thread.sleep(100)\n\n        assertTrue { testFunction.invoke() }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun memoizeSuspend0wrapper_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction = memoizeSuspend<Boolean> {\n            functionRunCount++\n            delay(100.milliseconds)\n            true\n        }\n\n        val result1 = testFunction.invoke()\n        val result2 = testFunction.invoke()\n        val result3 = testFunction.invoke()\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeSuspendExpiring0wrapper_onlyRunsOnce() = runBlocking {\n        // TODO: this should ideally use `runBlockingTest`, but that does not actually advance the time\n        var functionRunCount = 0\n\n        val testFunction = memoizeSuspend<Boolean>(50.milliseconds) {\n            functionRunCount++\n            delay(1.milliseconds)\n            true\n        }\n\n        val result1 = testFunction.invoke()\n        val result2 = testFunction.invoke()\n        val result3 = testFunction.invoke()\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n\n        assertEquals(1, functionRunCount)\n\n        delay(100.milliseconds)\n\n        val result4 = testFunction.invoke()\n        assertTrue { result4 }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoize0_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = {\n            functionRunCount++\n            true\n        }.memoized()\n\n        assertTrue { testFunction.invoke() }\n        assertTrue { testFunction.invoke() }\n        assertTrue { testFunction.invoke() }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeExpiring0_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = {\n            functionRunCount++\n            true\n        }.memoized(50.milliseconds)\n\n        assertTrue { testFunction.invoke() }\n        assertTrue { testFunction.invoke() }\n        assertTrue { testFunction.invoke() }\n\n        assertEquals(1, functionRunCount)\n\n        Thread.sleep(100)\n\n        assertTrue { testFunction.invoke() }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun memoizeSuspend0_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction = suspend {\n            functionRunCount++\n            delay(100.milliseconds)\n            true\n        }.memoizedSuspend()\n\n        val result1 = testFunction.invoke()\n        val result2 = testFunction.invoke()\n        val result3 = testFunction.invoke()\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeSuspendExpiring0_onlyRunsOnce() = runBlocking {\n        // TODO: this should ideally use `runBlockingTest`, but that does not actually advance the time\n        var functionRunCount = 0\n\n        val testFunction = suspend {\n            functionRunCount++\n            delay(1.milliseconds)\n            true\n        }.memoizedSuspend(50.milliseconds)\n\n        val result1 = testFunction.invoke()\n        val result2 = testFunction.invoke()\n        val result3 = testFunction.invoke()\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n\n        assertEquals(1, functionRunCount)\n\n        delay(100.milliseconds)\n\n        val result4 = testFunction.invoke()\n        assertTrue { result4 }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoize1wrapper_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = memoize { input: Int ->\n            functionRunCount++\n            input > 0\n        }\n\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(2) }\n        assertTrue { testFunction.invoke(2) }\n        assertTrue { testFunction.invoke(1) }\n\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeExpiring1wrapper_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = memoize(50.milliseconds) { input: Int ->\n            functionRunCount++\n            input > 0\n        }\n\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(1) }\n\n        assertEquals(1, functionRunCount)\n\n        Thread.sleep(100)\n\n        assertTrue { testFunction.invoke(1) }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoize1_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = { input: Int ->\n            functionRunCount++\n            input > 0\n        }.memoized()\n\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(2) }\n        assertTrue { testFunction.invoke(2) }\n        assertTrue { testFunction.invoke(1) }\n\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeExpiring1_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = { input: Int ->\n            functionRunCount++\n            input > 0\n        }.memoized(50.milliseconds)\n\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(1) }\n\n        assertEquals(1, functionRunCount)\n\n        Thread.sleep(100)\n\n        assertTrue { testFunction.invoke(1) }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun memoizeSuspend1wrapper_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction = memoizeSuspend { input: Int ->\n            functionRunCount++\n            delay(100.milliseconds)\n            input > 0\n        }\n\n        val result1 = testFunction.invoke(1)\n        val result2 = testFunction.invoke(1)\n        val result3 = testFunction.invoke(1)\n        val result4 = testFunction.invoke(2)\n        val result5 = testFunction.invoke(2)\n        val result6 = testFunction.invoke(1)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n        assertTrue { result4 }\n        assertTrue { result5 }\n        assertTrue { result6 }\n\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeSuspendExpiring1wrapper_onlyRunsOnce() = runBlocking {\n        // TODO: this should ideally use `runBlockingTest`, but that does not actually advance the time\n        var functionRunCount = 0\n\n        val testFunction = memoizeSuspend(50.milliseconds) { input: Int ->\n            functionRunCount++\n            delay(1.milliseconds)\n            input > 0\n        }\n\n        val result1 = testFunction.invoke(1)\n        val result2 = testFunction.invoke(1)\n        val result3 = testFunction.invoke(1)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n\n        assertEquals(1, functionRunCount)\n\n        delay(100.milliseconds)\n\n        val result4 = testFunction.invoke(1)\n        assertTrue { result4 }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun memoizeSuspend1_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction: suspend (Int) -> Boolean = { input ->\n            functionRunCount++\n            delay(100.milliseconds)\n            input > 0\n        }\n\n        val memoizedFunction = testFunction.memoizedSuspend()\n\n        val result1 = memoizedFunction.invoke(1)\n        val result2 = memoizedFunction.invoke(1)\n        val result3 = memoizedFunction.invoke(1)\n        val result4 = memoizedFunction.invoke(2)\n        val result5 = memoizedFunction.invoke(2)\n        val result6 = memoizedFunction.invoke(1)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n        assertTrue { result4 }\n        assertTrue { result5 }\n        assertTrue { result6 }\n\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeSuspendExpiring1_onlyRunsOnce() = runBlocking {\n        // TODO: this should ideally use `runBlockingTest`, but that does not actually advance the time\n        var functionRunCount = 0\n\n        val testFunction: suspend (Int) -> Boolean = { input: Int ->\n            functionRunCount++\n            delay(1.milliseconds)\n            input > 0\n        }\n\n        val memoizedFunction = testFunction.memoizedSuspend(50.milliseconds)\n\n        val result1 = memoizedFunction.invoke(1)\n        val result2 = memoizedFunction.invoke(1)\n        val result3 = memoizedFunction.invoke(1)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n\n        assertEquals(1, functionRunCount)\n\n        delay(100.milliseconds)\n\n        val result4 = memoizedFunction.invoke(1)\n        assertTrue { result4 }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoize2wrapper_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = memoize { input1: Int, input2: Int ->\n            functionRunCount++\n            input1 > 0 && input2 > 0\n        }\n\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 3) }\n        assertTrue { testFunction.invoke(2, 2) }\n        assertTrue { testFunction.invoke(4, 5) }\n        assertTrue { testFunction.invoke(1, 2) }\n\n        assertEquals(4, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeExpiring2wrapper_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = memoize(50.milliseconds) { input1: Int, input2: Int ->\n            functionRunCount++\n            input1 > 0 && input2 > 0\n        }\n\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 2) }\n\n        assertEquals(1, functionRunCount)\n\n        Thread.sleep(100)\n\n        assertTrue { testFunction.invoke(1, 2) }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoize2_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = { input1: Int, input2: Int ->\n            functionRunCount++\n            input1 > 0 && input2 > 0\n        }.memoized()\n\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 3) }\n        assertTrue { testFunction.invoke(2, 2) }\n        assertTrue { testFunction.invoke(4, 5) }\n        assertTrue { testFunction.invoke(1, 2) }\n\n        assertEquals(4, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeExpiring2_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = { input1: Int, input2: Int ->\n            functionRunCount++\n            input1 > 0 && input2 > 0\n        }.memoized(50.milliseconds)\n\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 2) }\n\n        assertEquals(1, functionRunCount)\n\n        Thread.sleep(100)\n\n        assertTrue { testFunction.invoke(1, 2) }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun memoizeSuspend2wrapper_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction = memoizeSuspend { input1: Int, input2: Int ->\n            functionRunCount++\n            delay(100.milliseconds)\n            input1 > 0 && input2 > 0\n        }\n\n        val result1 = testFunction.invoke(1, 2)\n        val result2 = testFunction.invoke(1, 2)\n        val result3 = testFunction.invoke(1, 2)\n        val result4 = testFunction.invoke(1, 3)\n        val result5 = testFunction.invoke(2, 2)\n        val result6 = testFunction.invoke(4, 5)\n        val result7 = testFunction.invoke(1, 2)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n        assertTrue { result4 }\n        assertTrue { result5 }\n        assertTrue { result6 }\n        assertTrue { result7 }\n\n        assertEquals(4, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeSuspendExpiring2wrapper_onlyRunsOnce() = runBlocking {\n        // TODO: this should ideally use `runBlockingTest`, but that does not actually advance the time\n        var functionRunCount = 0\n\n        val testFunction = memoizeSuspend(50.milliseconds) { input1: Int, input2: Int ->\n            functionRunCount++\n            delay(1.milliseconds)\n            input1 > 0 && input2 > 0\n        }\n\n        val result1 = testFunction.invoke(1, 2)\n        val result2 = testFunction.invoke(1, 2)\n        val result3 = testFunction.invoke(1, 2)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n\n        assertEquals(1, functionRunCount)\n\n        delay(100.milliseconds)\n\n        val result4 = testFunction.invoke(1, 2)\n        assertTrue { result4 }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun memoizeSuspend2_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction: suspend (Int, Int) -> Boolean = { input1, input2 ->\n            functionRunCount++\n            delay(100.milliseconds)\n            input1 > 0 && input2 > 0\n        }\n\n        val memoizedFunction = testFunction.memoizedSuspend()\n\n        val result1 = memoizedFunction.invoke(1, 2)\n        val result2 = memoizedFunction.invoke(1, 2)\n        val result3 = memoizedFunction.invoke(1, 2)\n        val result4 = memoizedFunction.invoke(1, 3)\n        val result5 = memoizedFunction.invoke(2, 2)\n        val result6 = memoizedFunction.invoke(4, 5)\n        val result7 = memoizedFunction.invoke(1, 2)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n        assertTrue { result4 }\n        assertTrue { result5 }\n        assertTrue { result6 }\n        assertTrue { result7 }\n\n        assertEquals(4, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeSuspendExpiring2_onlyRunsOnce() = runBlocking {\n        // TODO: this should ideally use `runBlockingTest`, but that does not actually advance the time\n        var functionRunCount = 0\n\n        val testFunction: suspend (Int, Int) -> Boolean = { input1, input2 ->\n            functionRunCount++\n            delay(1.milliseconds)\n            input1 > 0 && input2 > 0\n        }\n\n        val memoizedFunction = testFunction.memoizedSuspend(50.milliseconds)\n\n        val result1 = memoizedFunction.invoke(1, 2)\n        val result2 = memoizedFunction.invoke(1, 2)\n        val result3 = memoizedFunction.invoke(1, 2)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n\n        assertEquals(1, functionRunCount)\n\n        delay(100.milliseconds)\n\n        val result4 = memoizedFunction.invoke(1, 2)\n        assertTrue { result4 }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoize3wrapper_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = memoize { input1: Int, input2: Int, input3: Int ->\n            functionRunCount++\n            input1 > 0 && input2 > 0 && input3 > 0\n        }\n\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 3, 4) }\n        assertTrue { testFunction.invoke(2, 2, 5) }\n        assertTrue { testFunction.invoke(4, 5, 6) }\n        assertTrue { testFunction.invoke(1, 2, 7) }\n        assertTrue { testFunction.invoke(1, 2, 3) }\n\n        assertEquals(5, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeExpiring3wrapper_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = memoize(50.milliseconds) { input1: Int, input2: Int, input3: Int ->\n            functionRunCount++\n            input1 > 0 && input2 > 0 && input3 > 0\n        }\n\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 2, 3) }\n\n        assertEquals(1, functionRunCount)\n\n        Thread.sleep(100)\n\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoize3_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = { input1: Int, input2: Int, input3: Int ->\n            functionRunCount++\n            input1 > 0 && input2 > 0 && input3 > 0\n        }.memoized()\n\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 3, 4) }\n        assertTrue { testFunction.invoke(2, 2, 5) }\n        assertTrue { testFunction.invoke(4, 5, 6) }\n        assertTrue { testFunction.invoke(1, 2, 7) }\n        assertTrue { testFunction.invoke(1, 2, 3) }\n\n        assertEquals(5, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeExpiring3_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = { input1: Int, input2: Int, input3: Int ->\n            functionRunCount++\n            input1 > 0 && input2 > 0 && input3 > 0\n        }.memoized(50.milliseconds)\n\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 2, 3) }\n\n        assertEquals(1, functionRunCount)\n\n        Thread.sleep(100)\n\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun memoizeSuspend3wrapper_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction = memoizeSuspend { input1: Int, input2: Int, input3: Int ->\n            functionRunCount++\n            delay(100.milliseconds)\n            input1 > 0 && input2 > 0 && input3 > 0\n        }\n\n        val result1 = testFunction.invoke(1, 2, 3)\n        val result2 = testFunction.invoke(1, 2, 3)\n        val result3 = testFunction.invoke(1, 2, 3)\n        val result4 = testFunction.invoke(1, 3, 4)\n        val result5 = testFunction.invoke(2, 2, 5)\n        val result6 = testFunction.invoke(4, 5, 6)\n        val result7 = testFunction.invoke(1, 2, 7)\n        val result8 = testFunction.invoke(1, 2, 3)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n        assertTrue { result4 }\n        assertTrue { result5 }\n        assertTrue { result6 }\n        assertTrue { result7 }\n        assertTrue { result8 }\n\n        assertEquals(5, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeSuspendExpiring3wrapper_onlyRunsOnce() = runBlocking {\n        // TODO: this should ideally use `runBlockingTest`, but that does not actually advance the time\n        var functionRunCount = 0\n\n        val testFunction = memoizeSuspend(50.milliseconds) { input1: Int, input2: Int, input3: Int ->\n            functionRunCount++\n            delay(1.milliseconds)\n            input1 > 0 && input2 > 0 && input3 > 0\n        }\n\n        val result1 = testFunction.invoke(1, 2, 3)\n        val result2 = testFunction.invoke(1, 2, 3)\n        val result3 = testFunction.invoke(1, 2, 3)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n\n        assertEquals(1, functionRunCount)\n\n        delay(100.milliseconds)\n\n        val result4 = testFunction.invoke(1, 2, 3)\n        assertTrue { result4 }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun memoizeSuspend3_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction: suspend (Int, Int, Int) -> Boolean = { input1, input2, input3 ->\n            functionRunCount++\n            delay(100.milliseconds)\n            input1 > 0 && input2 > 0 && input3 > 0\n        }\n\n        val memoizedFunction = testFunction.memoizedSuspend()\n\n        val result1 = memoizedFunction.invoke(1, 2, 3)\n        val result2 = memoizedFunction.invoke(1, 2, 3)\n        val result3 = memoizedFunction.invoke(1, 2, 3)\n        val result4 = memoizedFunction.invoke(1, 3, 4)\n        val result5 = memoizedFunction.invoke(2, 2, 5)\n        val result6 = memoizedFunction.invoke(4, 5, 6)\n        val result7 = memoizedFunction.invoke(1, 2, 7)\n        val result8 = memoizedFunction.invoke(1, 2, 3)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n        assertTrue { result4 }\n        assertTrue { result5 }\n        assertTrue { result6 }\n        assertTrue { result7 }\n        assertTrue { result8 }\n\n        assertEquals(5, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun memoizeSuspendExpiring3_onlyRunsOnce() = runBlocking {\n        // TODO: this should ideally use `runBlockingTest`, but that does not actually advance the time\n        var functionRunCount = 0\n\n        val testFunction: suspend (Int, Int, Int) -> Boolean = { input1, input2, input3 ->\n            functionRunCount++\n            delay(1.milliseconds)\n            input1 > 0 && input2 > 0 && input3 > 0\n        }\n\n        val memoizedFunction = testFunction.memoizedSuspend(50.milliseconds)\n\n        val result1 = memoizedFunction.invoke(1, 2, 3)\n        val result2 = memoizedFunction.invoke(1, 2, 3)\n        val result3 = memoizedFunction.invoke(1, 2, 3)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n\n        assertEquals(1, functionRunCount)\n\n        delay(100.milliseconds)\n\n        val result4 = memoizedFunction.invoke(1, 2, 3)\n        assertTrue { result4 }\n        assertEquals(2, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun cacheFirstResult0wrapper_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = cacheFirstResult<Boolean> {\n            functionRunCount++\n            true\n        }\n\n        assertTrue { testFunction.invoke() }\n        assertTrue { testFunction.invoke() }\n        assertTrue { testFunction.invoke() }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun cacheFirstResult0_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = {\n            functionRunCount++\n            true\n        }.cachedFirstResult()\n\n        assertTrue { testFunction.invoke() }\n        assertTrue { testFunction.invoke() }\n        assertTrue { testFunction.invoke() }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun cacheFirstResultSuspend0wrapper_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction = cacheFirstResultSuspend<Boolean> {\n            functionRunCount++\n            delay(100.milliseconds)\n            true\n        }\n\n        val result1 = testFunction.invoke()\n        val result2 = testFunction.invoke()\n        val result3 = testFunction.invoke()\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun cachedFirstResultSuspend0_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction = suspend {\n            functionRunCount++\n            delay(100.milliseconds)\n            true\n        }.cachedFirstResultSuspend()\n\n        val result1 = testFunction.invoke()\n        val result2 = testFunction.invoke()\n        val result3 = testFunction.invoke()\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun cacheFirstResult1wrapper_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = cacheFirstResult { input: Int ->\n            functionRunCount++\n            input > 0\n        }\n\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(2) }\n        assertTrue { testFunction.invoke(2) }\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(-1) }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun cachedFirstResult1_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = { input: Int ->\n            functionRunCount++\n            input > 0\n        }.cachedFirstResult()\n\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(2) }\n        assertTrue { testFunction.invoke(2) }\n        assertTrue { testFunction.invoke(1) }\n        assertTrue { testFunction.invoke(-1) }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun cacheFirstResultSuspend1wrapper_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction = cacheFirstResultSuspend { input: Int ->\n            functionRunCount++\n            delay(100.milliseconds)\n            input > 0\n        }\n\n        val result1 = testFunction.invoke(1)\n        val result2 = testFunction.invoke(1)\n        val result3 = testFunction.invoke(1)\n        val result4 = testFunction.invoke(2)\n        val result5 = testFunction.invoke(2)\n        val result6 = testFunction.invoke(1)\n        val result7 = testFunction.invoke(-1)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n        assertTrue { result4 }\n        assertTrue { result5 }\n        assertTrue { result6 }\n        assertTrue { result7 }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun cachedFirstResultSuspend1_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction: suspend (Int) -> Boolean = { input ->\n            functionRunCount++\n            delay(100.milliseconds)\n            input > 0\n        }\n\n        val cachedFirstResultFunction = testFunction.cachedFirstResultSuspend()\n\n        val result1 = cachedFirstResultFunction.invoke(1)\n        val result2 = cachedFirstResultFunction.invoke(1)\n        val result3 = cachedFirstResultFunction.invoke(1)\n        val result4 = cachedFirstResultFunction.invoke(2)\n        val result5 = cachedFirstResultFunction.invoke(2)\n        val result6 = cachedFirstResultFunction.invoke(1)\n        val result7 = cachedFirstResultFunction.invoke(-1)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n        assertTrue { result4 }\n        assertTrue { result5 }\n        assertTrue { result6 }\n        assertTrue { result7 }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun cacheFirstResult2wrapper_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = cacheFirstResult { input1: Int, input2: Int ->\n            functionRunCount++\n            input1 > 0 && input2 > 0\n        }\n\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 3) }\n        assertTrue { testFunction.invoke(2, 2) }\n        assertTrue { testFunction.invoke(4, 5) }\n        assertTrue { testFunction.invoke(1, -1) }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun cachedFirstResult2_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = { input1: Int, input2: Int ->\n            functionRunCount++\n            input1 > 0 && input2 > 0\n        }.cachedFirstResult()\n\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 2) }\n        assertTrue { testFunction.invoke(1, 3) }\n        assertTrue { testFunction.invoke(2, 2) }\n        assertTrue { testFunction.invoke(4, 5) }\n        assertTrue { testFunction.invoke(1, -1) }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun cacheFirstResultSuspend2wrapper_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction = cacheFirstResultSuspend { input1: Int, input2: Int ->\n            functionRunCount++\n            delay(100.milliseconds)\n            input1 > 0 && input2 > 0\n        }\n\n        val result1 = testFunction.invoke(1, 2)\n        val result2 = testFunction.invoke(1, 2)\n        val result3 = testFunction.invoke(1, 2)\n        val result4 = testFunction.invoke(1, 3)\n        val result5 = testFunction.invoke(2, 2)\n        val result6 = testFunction.invoke(4, 5)\n        val result7 = testFunction.invoke(1, -1)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n        assertTrue { result4 }\n        assertTrue { result5 }\n        assertTrue { result6 }\n        assertTrue { result7 }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun cachedFirstResultSuspend2_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction: suspend (Int, Int) -> Boolean = { input1, input2 ->\n            functionRunCount++\n            delay(100.milliseconds)\n            input1 > 0 && input2 > 0\n        }\n\n        val cachedFirstResultFunction = testFunction.cachedFirstResultSuspend()\n\n        val result1 = cachedFirstResultFunction.invoke(1, 2)\n        val result2 = cachedFirstResultFunction.invoke(1, 2)\n        val result3 = cachedFirstResultFunction.invoke(1, 2)\n        val result4 = cachedFirstResultFunction.invoke(1, 3)\n        val result5 = cachedFirstResultFunction.invoke(2, 2)\n        val result6 = cachedFirstResultFunction.invoke(4, 5)\n        val result7 = cachedFirstResultFunction.invoke(1, -1)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n        assertTrue { result4 }\n        assertTrue { result5 }\n        assertTrue { result6 }\n        assertTrue { result7 }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun cacheFirstResult3wrapper_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = cacheFirstResult { input1: Int, input2: Int, input3: Int ->\n            functionRunCount++\n            input1 > 0 && input2 > 0 && input3 > 0\n        }\n\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 3, 4) }\n        assertTrue { testFunction.invoke(2, 2, 5) }\n        assertTrue { testFunction.invoke(4, 5, 6) }\n        assertTrue { testFunction.invoke(1, 2, 7) }\n        assertTrue { testFunction.invoke(1, 2, -1) }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    fun cachedFirstResult3_onlyRunsOnce() {\n        var functionRunCount = 0\n\n        val testFunction = { input1: Int, input2: Int, input3: Int ->\n            functionRunCount++\n            input1 > 0 && input2 > 0 && input3 > 0\n        }.cachedFirstResult()\n\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 2, 3) }\n        assertTrue { testFunction.invoke(1, 3, 4) }\n        assertTrue { testFunction.invoke(2, 2, 5) }\n        assertTrue { testFunction.invoke(4, 5, 6) }\n        assertTrue { testFunction.invoke(1, 2, 7) }\n        assertTrue { testFunction.invoke(1, 2, -1) }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun cacheFirstResultSuspend3wrapper_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction = cacheFirstResultSuspend { input1: Int, input2: Int, input3: Int ->\n            functionRunCount++\n            delay(100.milliseconds)\n            input1 > 0 && input2 > 0 && input3 > 0\n        }\n\n        val result1 = testFunction.invoke(1, 2, 3)\n        val result2 = testFunction.invoke(1, 2, 3)\n        val result3 = testFunction.invoke(1, 2, 3)\n        val result4 = testFunction.invoke(1, 3, 4)\n        val result5 = testFunction.invoke(2, 2, 5)\n        val result6 = testFunction.invoke(4, 5, 6)\n        val result7 = testFunction.invoke(1, 2, 7)\n        val result8 = testFunction.invoke(1, 2, -1)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n        assertTrue { result4 }\n        assertTrue { result5 }\n        assertTrue { result6 }\n        assertTrue { result7 }\n        assertTrue { result8 }\n\n        assertEquals(1, functionRunCount)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun cachedFirstResultSuspend3_onlyRunsOnce() = runBlockingTest {\n        var functionRunCount = 0\n\n        val testFunction: suspend (Int, Int, Int) -> Boolean = { input1, input2, input3 ->\n            functionRunCount++\n            delay(100.milliseconds)\n            input1 > 0 && input2 > 0 && input3 > 0\n        }\n\n        val cachedFirstResultFunction = testFunction.cachedFirstResultSuspend()\n\n        val result1 = cachedFirstResultFunction.invoke(1, 2, 3)\n        val result2 = cachedFirstResultFunction.invoke(1, 2, 3)\n        val result3 = cachedFirstResultFunction.invoke(1, 2, 3)\n        val result4 = cachedFirstResultFunction.invoke(1, 3, 4)\n        val result5 = cachedFirstResultFunction.invoke(2, 2, 5)\n        val result6 = cachedFirstResultFunction.invoke(4, 5, 6)\n        val result7 = cachedFirstResultFunction.invoke(1, 2, 7)\n        val result8 = cachedFirstResultFunction.invoke(1, 2, -1)\n\n        assertTrue { result1 }\n        assertTrue { result2 }\n        assertTrue { result3 }\n        assertTrue { result4 }\n        assertTrue { result5 }\n        assertTrue { result6 }\n        assertTrue { result7 }\n        assertTrue { result8 }\n\n        assertEquals(1, functionRunCount)\n    }\n}\n"
  },
  {
    "path": "scan-framework/src/test/java/com/getbouncer/scan/framework/util/RetryTest.kt",
    "content": "package com.getbouncer.scan.framework.util\n\nimport androidx.test.filters.SmallTest\nimport com.getbouncer.scan.framework.time.milliseconds\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runBlockingTest\nimport org.junit.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFailsWith\n\nclass RetryTest {\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun retry_succeedsFirst() = runBlockingTest {\n        var executions = 0\n\n        assertEquals(\n            1,\n            retry(1.milliseconds) {\n                executions++\n                1\n            }\n        )\n        assertEquals(1, executions)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun retry_succeedsSecond() = runBlockingTest {\n        var executions = 0\n\n        assertEquals(\n            1,\n            retry(1.milliseconds) {\n                executions++\n                if (executions == 2) {\n                    1\n                } else {\n                    throw RuntimeException()\n                }\n            }\n        )\n        assertEquals(2, executions)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun retry_fails() = runBlockingTest {\n        var executions = 0\n\n        assertFailsWith<RuntimeException> {\n            retry<Int>(1.milliseconds) {\n                executions++\n                throw RuntimeException()\n            }\n        }\n        assertEquals(3, executions)\n    }\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun retry_excluding() = runBlockingTest {\n        var executions = 0\n\n        assertFailsWith<RuntimeException> {\n            retry<Int>(1.milliseconds, excluding = listOf(RuntimeException::class.java)) {\n                executions++\n                throw RuntimeException()\n            }\n        }\n        assertEquals(1, executions)\n    }\n}\n"
  },
  {
    "path": "scan-payment/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "scan-payment/README.md",
    "content": "# Deprecation Notice\nHello from the Stripe (formerly Bouncer) team!\n\nWe're excited to provide an update on the state and future of the [Card Scan OCR](https://github.com/stripe/stripe-android/tree/master/stripecardscan) product! As we continue to build into Stripe's ecosystem, we'll be supporting the mission to continuously improve the end customer experience in many of Stripe's core checkout products.\n\nThis SDK has been [migrated to Stripe](https://github.com/stripe/stripe-android/tree/master/stripecardscan) and is now free for use under the MIT license!\n\nIf you are not currently a Stripe user, and interested in learning more about improving checkout experience through Stripe, please let us know and we can connect you with the team.\n\nIf you are not currently a Stripe user, and want to continue using the existing SDK, you can do so free of charge. Starting January 1, 2022, we will no longer be charging for use of the existing Bouncer Card Scan OCR SDK. For product support on [Android](https://github.com/stripe/stripe-android/issues) and [iOS](https://github.com/stripe/stripe-ios/issues). For billing support, please email [bouncer-support@stripe.com](mailto:bouncer-support@stripe.com).\n\nFor the new product, please visit the [stripe github repository](https://github.com/stripe/stripe-android/tree/master/stripecardscan).\n\n# scan-payment\nThis repository contains the legacy, deprecated open source machine learning models and payment card utilities needed to quickly and accurately scan payment cards. [CardScan](https://cardscan.io/) is a relatively small library that provides fast and accurate payment card scanning.\n\nNote this library does not contain any user interfaces. Another library, [cardscan-ui](https://github.com/getbouncer/cardscan-ui-android) builds upon this one any adds simple user interfaces. \n\nscan-payment serves as the foundation for CardScan and CardVerify enterprise libraries, which validate the authenticity of payment cards as they are scanned.\n\n![demo](../docs/images/demo.gif)\n\n## Contents\n* [Requirements](#requirements)\n* [Demo](#demo)\n* [Integration](#integration)\n* [Using](#using)\n* [Developing](#developing)\n* [Authors](#authors)\n* [License](#license)\n\n## Requirements\n* Android API level 21 or higher\n* Kotlin coroutine compatibility\n\nNote: Your app does not have to be written in kotlin to integrate this library, but must be able to depend on kotlin functionality.\n\n## Demo\nAn app demonstrating the basic capabilities of CardScan is available in [github](https://github.com/getbouncer/cardscan-demo-android).\n\n## Integration\nSee the [integration documentation](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) in the Bouncer Docs.\n\n## Using\nThis library is designed to be used with [cardscan-ui](https://github.com/getbouncer/cardscan-ui-android), which will provide user interfaces for scanning payment cards. However, it can be used independently.\n\nFor an overview of the architecture and design of the scan framework, see the [architecture documentation](https://docs.getbouncer.com/card-scan/android-integration-guide/android-architecture-overview).\n\n## Developing\nSee the [development docs](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) for details on developing this library.\n\n## Authors\nAdam Wushensky, Sam King, and Zain ul Abi Din\n\n## License\nThis library is available under the MIT license. See the [LICENSE](../LICENSE) file for the full license text.\n"
  },
  {
    "path": "scan-payment/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\napply plugin: 'kotlinx-serialization'\napply plugin: 'kotlin-parcelize'\n\nandroid {\n    compileSdkVersion 30\n    buildToolsVersion '30.0.3'\n\n    defaultConfig {\n        minSdkVersion 21\n        targetSdkVersion 30\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles \"consumer-rules.pro\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile(\"proguard-android-optimize.txt\"), \"proguard-rules.pro\"\n        }\n    }\n\n    testOptions {\n        unitTests.includeAndroidResources = true\n    }\n\n    lintOptions {\n        enable \"Interoperability\"\n    }\n\n    packagingOptions {\n        pickFirst 'META-INF/AL2.0'\n        pickFirst 'META-INF/LGPL2.1'\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = JavaVersion.VERSION_1_8.toString()\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: \"libs\", include: [\"*.jar\"])\n    implementation project(\":scan-framework\")\n\n    implementation \"androidx.core:core-ktx:[1.3.1,1.6.0]\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-core:[1.4.0,1.5.1]\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-android:[1.4.0,1.5.1]\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-serialization-json:[1.1.0,1.2.2]\"\n\n    // Allow the user to specify their own version of Tensorflow Lite to include\n    runtimeOnly project(\":tensorflow-lite\")\n    compileOnly \"org.tensorflow:tensorflow-lite:2.4.0\"\n}\n\ndependencies {\n    testImplementation \"androidx.test:core:1.4.0\"\n    testImplementation \"androidx.test:runner:1.4.0\"\n    testImplementation \"junit:junit:4.13.2\"\n    testImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n}\n\ndependencies {\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.4.0\"\n    androidTestImplementation \"androidx.test.ext:junit:1.1.3\"\n    androidTestImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n    androidTestImplementation \"org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1\"\n}\n\napply from: \"deploy.gradle\"\n"
  },
  {
    "path": "scan-payment/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "scan-payment/deploy.gradle",
    "content": "apply plugin: 'maven-publish'\napply plugin: 'org.jetbrains.dokka'\napply plugin: 'signing'\n\ntask androidSourcesJar(type: Jar) {\n    archiveClassifier.set('sources')\n    if (project.plugins.findPlugin(\"com.android.library\")) {\n        // Android library\n        from android.sourceSets.main.java.srcDirs\n        from android.sourceSets.main.kotlin.srcDirs\n    } else {\n        // Pure kotlin library\n        from sourceSets.main.java.srcDirs\n        from sourceSets.main.kotlin.srcDirs\n    }\n}\n\ntasks.withType(dokkaHtmlPartial.getClass()).configureEach {\n    pluginsMapConfiguration.set(\n            [\"org.jetbrains.dokka.base.DokkaBase\": \"\"\"{ \"separateInheritedMembers\": true}\"\"\"]\n    )\n}\n\ntask javadocJar(type: Jar, dependsOn: dokkaJavadoc) {\n    archiveClassifier.set('javadoc')\n    from dokkaJavadoc.outputDirectory\n}\n\nartifacts {\n    archives androidSourcesJar\n    archives javadocJar\n}\n\next[\"signing.keyId\"] = ''\next[\"signing.password\"] = ''\next[\"signing.secretKeyRingFile\"] = ''\n\next[\"ossrhUsername\"] = ''\next[\"ossrhPassword\"] = ''\next[\"sonatypeStagingProfileId\"] = ''\n\next {\n\n    libraryDescription = 'This library provides the framework for scanning payment cards'\n\n    siteUrl = 'https://getbouncer.com'\n\n    scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'\n    scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'\n    scmUrl = 'https://github.com/getbouncer/cardscan-android'\n\n    licenseName = 'bouncer-free-1'\n    licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'\n\n    developerId = 'getbouncer'\n    developerName = 'Bouncer Technologies'\n    developerEmail = 'bouncer-support@stripe.com'\n\n    publishGroupId = 'com.getbouncer'\n    publishArtifactId = 'scan-payment'\n    publishVersion = version\n}\n\ngroup = publishGroupId\nversion = publishVersion\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    Properties p = new Properties()\n    new FileInputStream(secretPropsFile).withCloseable { is ->\n        p.load(is)\n    }\n    p.each { name, value ->\n        ext[name] = value\n    }\n} else {\n    ext[\"signing.keyId\"] = System.getenv('SIGNING_KEY_ID')\n    ext[\"signing.password\"] = System.getenv('SIGNING_PASSWORD')\n    ext[\"signing.secretKeyRingFile\"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')\n\n    ext[\"ossrhUsername\"] = System.getenv('OSSRH_USERNAME')\n    ext[\"ossrhPassword\"] = System.getenv('OSSRH_PASSWORD')\n\n    ext[\"sonatypeStagingProfileId\"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')\n}\n\npublishing {\n    publications {\n        release(MavenPublication) {\n            groupId publishGroupId\n            artifactId publishArtifactId\n            version publishVersion\n\n            // Two artifacts, the `aar` (or `jar`) and the sources\n            if (project.plugins.findPlugin(\"com.android.library\")) {\n                artifact(\"$buildDir/outputs/aar/${project.getName()}-release.aar\")\n            } else {\n                artifact(\"$buildDir/libs/${project.getName()}-${version}.jar\")\n            }\n            artifact androidSourcesJar\n\n            pom {\n                name = publishArtifactId\n                description = libraryDescription\n                url = siteUrl\n                licenses {\n                    license {\n                        name = licenseName\n                        url = licenseUrl\n                    }\n                }\n                developers {\n                    developer {\n                        id = developerId\n                        name = developerName\n                        email = developerEmail\n                    }\n                }\n                scm {\n                    connection = scmConnection\n                    developerConnection = scmDeveloperConnection\n                    url = scmUrl\n                }\n                // A slightly hacky fix so that your POM will include any transitive dependencies\n                // that your library builds upon\n                withXml {\n                    def dependenciesNode = asNode().appendNode('dependencies')\n\n                    project.configurations.implementation.allDependencies.each {\n                        if (it.group != null && it.version != null) {\n                            def dependencyNode = dependenciesNode.appendNode('dependency')\n                            dependencyNode.appendNode('groupId', it.group)\n                            dependencyNode.appendNode('artifactId', it.name)\n                            dependencyNode.appendNode('version', it.version)\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // The repository to publish to, Sonatype/MavenCentral\n    repositories {\n        maven {\n            // This is an arbitrary name, you may also use \"mavencentral\" or\n            // any other name that's descriptive for you\n            name = \"sonatype\"\n            url = \"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/\"\n            credentials {\n                username ossrhUsername\n                password ossrhPassword\n            }\n        }\n    }\n}\n\nsigning {\n    sign publishing.publications\n}\n"
  },
  {
    "path": "scan-payment/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\n-keep class org.tensorflow.lite.Interpreter { *; }\n"
  },
  {
    "path": "scan-payment/src/androidTest/java/com/getbouncer/scan/payment/ImageTest.kt",
    "content": "package com.getbouncer.scan.payment\n\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport android.graphics.Paint\nimport android.graphics.Rect\nimport android.graphics.RectF\nimport android.util.Size\nimport androidx.core.graphics.drawable.toBitmap\nimport androidx.test.filters.SmallTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.getbouncer.scan.framework.image.crop\nimport com.getbouncer.scan.framework.image.cropWithFill\nimport com.getbouncer.scan.framework.image.scale\nimport com.getbouncer.scan.framework.image.size\nimport com.getbouncer.scan.framework.image.toMLImage\nimport com.getbouncer.scan.framework.image.zoom\nimport com.getbouncer.scan.framework.util.centerOn\nimport com.getbouncer.scan.framework.util.toRect\nimport com.getbouncer.scan.payment.test.R\nimport org.junit.Test\nimport java.nio.ByteBuffer\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertTrue\n\nclass ImageTest {\n\n    private val testResources = InstrumentationRegistry.getInstrumentation().context.resources\n\n    @Test\n    @SmallTest\n    fun bitmap_toRGBByteBuffer_isCorrect() {\n        val bitmap = generateSampleBitmap()\n        assertNotNull(bitmap)\n        assertEquals(100, bitmap.width, \"Bitmap width is not expected\")\n        assertEquals(100, bitmap.height, \"Bitmap height is not expected\")\n\n        // convert the bitmap to a byte buffer\n        val convertedImage = bitmap.toMLImage(mean = 127.5f, std = 128.5f).getData()\n\n        // read in an expected converted file\n        val rawStream = testResources.openRawResource(R.raw.sample_bitmap)\n        val rawBytes = rawStream.readBytes()\n        val rawImage = ByteBuffer.wrap(rawBytes)\n        rawStream.close()\n\n        // check the size of the files\n        assertEquals(rawImage.limit(), convertedImage.limit(), \"File size mismatch\")\n        rawImage.rewind()\n        convertedImage.rewind()\n\n        // check each byte of the files\n        var encounteredNonZeroByte = false\n        while (convertedImage.position() < convertedImage.limit()) {\n            val rawImageByte = rawImage.get()\n            encounteredNonZeroByte = encounteredNonZeroByte || rawImageByte.toInt() != 0\n            assertEquals(rawImageByte, convertedImage.get(), \"Difference at byte ${rawImage.position()}\")\n        }\n\n        assertTrue(encounteredNonZeroByte, \"Bytes were all zero\")\n    }\n\n    @Test\n    @SmallTest\n    fun bitmap_scale_isCorrect() {\n        // read in a sample bitmap file\n        val bitmap = testResources.getDrawable(R.drawable.ocr_card_numbers, null).toBitmap()\n        assertNotNull(bitmap)\n        assertEquals(600, bitmap.width, \"Bitmap width is not expected\")\n        assertEquals(375, bitmap.height, \"Bitmap height is not expected\")\n\n        // scale the bitmap\n        val scaledBitmap = bitmap.scale(Size(bitmap.width / 5, bitmap.height / 5))\n\n        // check the expected sizes of the images\n        assertEquals(\n            Size(bitmap.width / 5, bitmap.height / 5),\n            Size(scaledBitmap.width, scaledBitmap.height),\n            \"Scaled image is the wrong size\"\n        )\n\n        // check each pixel of the images\n        var encounteredNonZeroPixel = false\n        for (x in 0 until scaledBitmap.width) {\n            for (y in 0 until scaledBitmap.height) {\n                encounteredNonZeroPixel = encounteredNonZeroPixel || scaledBitmap.getPixel(x, y) != 0\n            }\n        }\n\n        assertTrue(encounteredNonZeroPixel, \"Pixels were all zero\")\n    }\n\n    @Test\n    @SmallTest\n    fun bitmap_crop_isCorrect() {\n        val bitmap = testResources.getDrawable(R.drawable.ocr_card_numbers, null).toBitmap()\n        assertNotNull(bitmap)\n        assertEquals(600, bitmap.width, \"Bitmap width is not expected\")\n        assertEquals(375, bitmap.height, \"Bitmap height is not expected\")\n\n        // crop the bitmap\n        val croppedBitmap = bitmap.crop(\n            Rect(\n                bitmap.width / 4,\n                bitmap.height / 4,\n                bitmap.width * 3 / 4,\n                bitmap.height * 3 / 4\n            )\n        )\n\n        // check the expected sizes of the images\n        assertEquals(\n            Size(bitmap.width * 3 / 4 - bitmap.width / 4, bitmap.height * 3 / 4 - bitmap.height / 4),\n            Size(croppedBitmap.width, croppedBitmap.height),\n            \"Cropped image is the wrong size\"\n        )\n\n        // check each pixel of the images\n        var encounteredNonZeroPixel = false\n        for (x in 0 until croppedBitmap.width) {\n            for (y in 0 until croppedBitmap.height) {\n                val croppedPixel = croppedBitmap.getPixel(x, y)\n                val originalPixel = bitmap.getPixel(x + bitmap.width / 4, y + bitmap.height / 4)\n                assertEquals(originalPixel, croppedPixel, \"Difference at pixel $x, $y\")\n                encounteredNonZeroPixel = encounteredNonZeroPixel || croppedPixel != 0\n            }\n        }\n\n        assertTrue(encounteredNonZeroPixel, \"Pixels were all zero\")\n    }\n\n    @Test\n    @SmallTest\n    fun bitmap_cropWithFill_isCorrect() {\n        val bitmap = testResources.getDrawable(R.drawable.ocr_card_numbers, null).toBitmap()\n        assertNotNull(bitmap)\n        assertEquals(600, bitmap.width, \"Bitmap width is not expected\")\n        assertEquals(375, bitmap.height, \"Bitmap height is not expected\")\n\n        val cropRegion = Rect(\n            -100,\n            -100,\n            bitmap.width + 100,\n            bitmap.height + 100\n        )\n\n        // crop the bitmap\n        val croppedBitmap = bitmap.cropWithFill(\n            cropRegion\n        )\n\n        // check the expected sizes of the images\n        assertEquals(\n            Size(\n                bitmap.width + 200,\n                bitmap.height + 200\n            ),\n            Size(croppedBitmap.width, croppedBitmap.height),\n            \"Cropped image is the wrong size\"\n        )\n\n        for (y in 0 until croppedBitmap.height) {\n            for (x in 0 until croppedBitmap.width) {\n                if (x < 100 || x > 700 || y < 100 || y > 475) {\n                    val croppedPixel = croppedBitmap.getPixel(x, y)\n                    assertEquals(Color.GRAY, croppedPixel, \"Pixel $x, $y not gray\")\n                }\n            }\n        }\n\n        // check each pixel of the images\n        var encounteredNonZeroPixel = false\n        for (x in 0 until bitmap.width) {\n            for (y in 0 until bitmap.height) {\n                val croppedPixel = croppedBitmap.getPixel(x + 100, y + 100)\n                val originalPixel = bitmap.getPixel(x, y)\n                assertEquals(originalPixel, croppedPixel, \"Difference at pixel $x, $y\")\n                encounteredNonZeroPixel = encounteredNonZeroPixel || croppedPixel != 0\n            }\n        }\n\n        assertTrue(encounteredNonZeroPixel, \"Pixels were all zero\")\n    }\n\n    @Test\n    @SmallTest\n    fun zoom_isCorrect() {\n        val bitmap = testResources.getDrawable(R.drawable.ocr_card_numbers, null).toBitmap()\n        assertNotNull(bitmap)\n        assertEquals(600, bitmap.width, \"Bitmap width is not expected\")\n        assertEquals(375, bitmap.height, \"Bitmap height is not expected\")\n\n        // zoom the bitmap\n        val zoomedBitmap = bitmap.zoom(\n            originalRegion = Size(224, 224).centerOn(bitmap.size().toRect()),\n            newRegion = Rect(112, 112, 336, 336),\n            newImageSize = Size(448, 448)\n        )\n\n        // check the expected sizes of the images\n        assertEquals(\n            Size(448, 448),\n            Size(zoomedBitmap.width, zoomedBitmap.height),\n            \"Zoomed image is the wrong size\"\n        )\n\n        // check each pixel of the images\n        var encounteredNonZeroPixel = false\n        for (x in 0 until zoomedBitmap.width) {\n            for (y in 0 until zoomedBitmap.height) {\n                val zoomedPixel = zoomedBitmap.getPixel(x, y)\n                encounteredNonZeroPixel = encounteredNonZeroPixel || zoomedPixel != 0\n            }\n        }\n\n        assertTrue(encounteredNonZeroPixel, \"Pixels were all zero\")\n    }\n\n    private fun generateSampleBitmap(size: Size = Size(100, 100)): Bitmap {\n        val paint = Paint(Paint.ANTI_ALIAS_FLAG)\n        val bitmap = Bitmap.createBitmap(size.width, size.height, Bitmap.Config.ARGB_8888)\n        val canvas = Canvas(bitmap)\n\n        for (x in 0 until size.width) {\n            for (y in 0 until size.height) {\n                val red = 255 * x / size.width\n                val green = 255 * y / size.height\n                val blue = 255 * x / size.height\n                paint.color = Color.rgb(red, green, blue)\n                canvas.drawRect(RectF(x.toFloat(), y.toFloat(), x + 1F, y + 1F), paint)\n            }\n        }\n\n        canvas.drawBitmap(bitmap, 0F, 0F, paint)\n\n        return bitmap\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/androidTest/java/com/getbouncer/scan/payment/PaymentCardAndroidTest.kt",
    "content": "package com.getbouncer.scan.payment\n\nimport androidx.test.filters.SmallTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.getbouncer.scan.payment.card.CardType\nimport com.getbouncer.scan.payment.card.getCardType\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.runBlocking\nimport org.junit.Test\nimport kotlin.test.assertEquals\n\nclass PaymentCardAndroidTest {\n\n    private val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n\n    @Test\n    @SmallTest\n    @ExperimentalCoroutinesApi\n    fun paymentCardTypeDebit() = runBlocking {\n        assertEquals(CardType.Debit, getCardType(appContext, \"349011\"))\n        assertEquals(CardType.Credit, getCardType(appContext, \"648298\"))\n        assertEquals(CardType.Credit, getCardType(appContext, \"648299\"))\n        assertEquals(CardType.Prepaid, getCardType(appContext, \"531306\"))\n        assertEquals(CardType.Unknown, getCardType(appContext, \"123456\"))\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/androidTest/java/com/getbouncer/scan/payment/ml/CardDetectTest.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport androidx.core.graphics.drawable.toBitmap\nimport androidx.test.filters.MediumTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.Stats\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.UpdatingModelWebFetcher\nimport com.getbouncer.scan.framework.UpdatingResourceFetcher\nimport com.getbouncer.scan.framework.image.size\nimport com.getbouncer.scan.framework.util.toRect\nimport com.getbouncer.scan.payment.test.R\nimport kotlinx.coroutines.runBlocking\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFalse\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertTrue\n\nclass CardDetectTest {\n    private val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n    private val testContext = InstrumentationRegistry.getInstrumentation().context\n\n    @Before\n    fun before() {\n        Config.apiKey = \"qOJ_fF-WLDMbG05iBq5wvwiTNTmM2qIn\"\n    }\n\n    @After\n    fun after() {\n        Config.apiKey = null\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun cardDetect_pan() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.card_pan, null).toBitmap()\n        val fetcher = CardDetectModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertFalse(fetcher is UpdatingResourceFetcher)\n        assertTrue(fetcher is UpdatingModelWebFetcher)\n        fetcher.clearCache()\n\n        val model = CardDetect.Factory(appContext, fetcher.fetchData(forImmediateUse = true, isOptional = false)).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            CardDetect.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        assertEquals(CardDetect.Prediction.Side.PAN, prediction.side)\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun cardDetect_noPan() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.card_no_pan, null).toBitmap()\n        val fetcher = CardDetectModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertFalse(fetcher is UpdatingResourceFetcher)\n        assertTrue(fetcher is UpdatingModelWebFetcher)\n        fetcher.clearCache()\n\n        val model = CardDetect.Factory(appContext, fetcher.fetchData(forImmediateUse = true, isOptional = false)).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            CardDetect.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        assertEquals(CardDetect.Prediction.Side.NO_PAN, prediction.side)\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun cardDetect_noCard() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.card_no_card, null).toBitmap()\n        val fetcher = CardDetectModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertFalse(fetcher is UpdatingResourceFetcher)\n        assertTrue(fetcher is UpdatingModelWebFetcher)\n        fetcher.clearCache()\n\n        val model = CardDetect.Factory(appContext, fetcher.fetchData(forImmediateUse = true, isOptional = false)).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            CardDetect.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        assertEquals(CardDetect.Prediction.Side.NO_CARD, prediction.side)\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/androidTest/java/com/getbouncer/scan/payment/ml/ExpiryDetectTest.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport androidx.test.filters.SmallTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.getbouncer.scan.framework.Config\nimport kotlinx.coroutines.runBlocking\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\nimport kotlin.test.assertNotNull\n\nclass ExpiryDetectTest {\n    private val testContext = InstrumentationRegistry.getInstrumentation().context\n\n    @Before\n    fun before() {\n        Config.apiKey = \"qOJ_fF-WLDMbG05iBq5wvwiTNTmM2qIn\"\n    }\n\n    @After\n    fun after() {\n        Config.apiKey = null\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @SmallTest\n    fun createsInterpreter() = runBlocking {\n        val fetcher = ExpiryDetect.ModelFetcher(testContext)\n        fetcher.clearCache()\n\n        val factory = ExpiryDetect.Factory(testContext, fetcher.fetchData(forImmediateUse = false, isOptional = false))\n\n        assertNotNull(factory.newInstance())\n    }.let { }\n\n    @Test\n    @SmallTest\n    fun createsValidOutput() {\n        // TODO: add resources and test the object detector\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/androidTest/java/com/getbouncer/scan/payment/ml/SSDOcrTest.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport androidx.core.graphics.drawable.toBitmap\nimport androidx.test.filters.MediumTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.Stats\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.UpdatingModelWebFetcher\nimport com.getbouncer.scan.framework.UpdatingResourceFetcher\nimport com.getbouncer.scan.framework.image.size\nimport com.getbouncer.scan.framework.util.toRect\nimport com.getbouncer.scan.payment.test.R\nimport kotlinx.coroutines.runBlocking\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFalse\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertTrue\n\nclass SSDOcrTest {\n    private val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n    private val testContext = InstrumentationRegistry.getInstrumentation().context\n\n    @Before\n    fun before() {\n        Config.apiKey = \"qOJ_fF-WLDMbG05iBq5wvwiTNTmM2qIn\"\n    }\n\n    @After\n    fun after() {\n        Config.apiKey = null\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun resourceModelExecution_works() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.ocr_card_numbers, null).toBitmap()\n        val fetcher = SSDOcrModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertFalse(fetcher is UpdatingResourceFetcher)\n        assertTrue(fetcher is UpdatingModelWebFetcher)\n        fetcher.clearCache()\n\n        val model = SSDOcr.Factory(appContext, fetcher.fetchData(forImmediateUse = true, isOptional = false)).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            SSDOcr.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        assertEquals(\"3023334877861104\", prediction.pan)\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun resourceModelExecution_worksWithQR() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.ocr_card_numbers_qr, null).toBitmap()\n        val fetcher = SSDOcrModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertFalse(fetcher is UpdatingResourceFetcher)\n        assertTrue(fetcher is UpdatingModelWebFetcher)\n        fetcher.clearCache()\n\n        val model = SSDOcr.Factory(appContext, fetcher.fetchData(forImmediateUse = true, isOptional = false)).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            SSDOcr.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        assertEquals(\"4242424242424242\", prediction.pan)\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun resourceModelExecution_worksRepeatedly() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.ocr_card_numbers, null).toBitmap()\n        val fetcher = SSDOcrModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertFalse(fetcher is UpdatingResourceFetcher)\n        assertTrue(fetcher is UpdatingModelWebFetcher)\n        fetcher.clearCache()\n\n        val model = SSDOcr.Factory(appContext, fetcher.fetchData(forImmediateUse = true, isOptional = false)).newInstance()\n        assertNotNull(model)\n\n        val prediction1 = model.analyze(\n            SSDOcr.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        val prediction2 = model.analyze(\n            SSDOcr.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction1)\n        assertEquals(\"3023334877861104\", prediction1.pan)\n\n        assertNotNull(prediction2)\n        assertEquals(\"3023334877861104\", prediction2.pan)\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/androidTest/java/com/getbouncer/scan/payment/ml/TextDetectTest.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport androidx.test.filters.SmallTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.getbouncer.scan.framework.Config\nimport kotlinx.coroutines.runBlocking\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\nimport kotlin.test.assertNotNull\n\nclass TextDetectTest {\n    private val testContext = InstrumentationRegistry.getInstrumentation().context\n\n    @Before\n    fun before() {\n        Config.apiKey = \"qOJ_fF-WLDMbG05iBq5wvwiTNTmM2qIn\"\n    }\n\n    @After\n    fun after() {\n        Config.apiKey = null\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @SmallTest\n    fun createsInterpreter() = runBlocking {\n        val fetcher = TextDetect.ModelFetcher(testContext)\n        fetcher.clearCache()\n\n        val factory = TextDetect.Factory(testContext, fetcher.fetchData(forImmediateUse = false, isOptional = false))\n\n        assertNotNull(factory.newInstance())\n    }.let { }\n\n    @Test\n    @SmallTest\n    fun createsValidOutput() {\n        // TODO: add resources and test the object detector\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/androidTest/java/com/getbouncer/scan/payment/ml/ssd/SSDTest.kt",
    "content": "package com.getbouncer.scan.payment.ml.ssd\n\nimport android.graphics.RectF\nimport com.getbouncer.scan.payment.ml.VERTICAL_THRESHOLD\nimport org.junit.Test\nimport kotlin.test.assertEquals\n\nclass SSDTest {\n\n    @Test\n    fun determineLayoutAndFilter_emptyBoxes() {\n        determineLayoutAndFilter(emptyList(), VERTICAL_THRESHOLD)\n    }\n\n    @Test\n    fun determineLayoutAndFilter_linearNumbers() {\n        val numbers = listOf(\n            DetectionBox(\n                rect = RectF(0.00F, 0.00F, 0.00F, 0.00F),\n                confidence = 1F,\n                label = 0,\n            ),\n            DetectionBox(\n                rect = RectF(0.01F, 0.00F, 0.01F, 0.00F),\n                confidence = 1F,\n                label = 1,\n            ),\n            DetectionBox(\n                rect = RectF(0.02F, 0.00F, 0.02F, 0.00F),\n                confidence = 1F,\n                label = 2,\n            ),\n            DetectionBox(\n                rect = RectF(0.03F, 0.00F, 0.03F, 0.00F),\n                confidence = 1F,\n                label = 3,\n            ),\n            DetectionBox(\n                rect = RectF(0.04F, 0.00F, 0.04F, 0.00F),\n                confidence = 1F,\n                label = 4,\n            ),\n            DetectionBox(\n                rect = RectF(0.05F, 0.00F, 0.05F, 0.00F),\n                confidence = 1F,\n                label = 5,\n            ),\n            DetectionBox(\n                rect = RectF(0.06F, 0.00F, 0.06F, 0.00F),\n                confidence = 1F,\n                label = 6,\n            ),\n            DetectionBox(\n                rect = RectF(0.07F, 0.00F, 0.07F, 0.00F),\n                confidence = 1F,\n                label = 7,\n            ),\n            DetectionBox(\n                rect = RectF(0.08F, 0.00F, 0.08F, 0.00F),\n                confidence = 1F,\n                label = 8,\n            ),\n            DetectionBox(\n                rect = RectF(0.09F, 0.00F, 0.09F, 0.00F),\n                confidence = 1F,\n                label = 9,\n            ),\n            DetectionBox(\n                rect = RectF(0.10F, 0.00F, 0.10F, 0.00F),\n                confidence = 1F,\n                label = 0,\n            ),\n            DetectionBox(\n                rect = RectF(0.11F, 0.00F, 0.11F, 0.00F),\n                confidence = 1F,\n                label = 1,\n            ),\n            DetectionBox(\n                rect = RectF(0.12F, 0.00F, 0.12F, 0.00F),\n                confidence = 1F,\n                label = 2,\n            ),\n            DetectionBox(\n                rect = RectF(0.13F, 0.00F, 0.13F, 0.00F),\n                confidence = 1F,\n                label = 3,\n            ),\n            DetectionBox(\n                rect = RectF(0.14F, 0.00F, 0.14F, 0.00F),\n                confidence = 1F,\n                label = 4,\n            ),\n            DetectionBox(\n                rect = RectF(0.15F, 0.00F, 0.15F, 0.00F),\n                confidence = 1F,\n                label = 5,\n            ),\n        )\n\n        assertEquals(\"0123456789012345\", determineLayoutAndFilter(numbers, VERTICAL_THRESHOLD).map { it.label }.joinToString(\"\"))\n    }\n\n    @Test\n    fun determineLayoutAndFilter_visaQuickReadNumbers() {\n        val numbers = listOf(\n            DetectionBox(\n                rect = RectF(0.00F, 0.00F, 0.00F, 0.00F),\n                confidence = 1F,\n                label = 0,\n            ),\n            DetectionBox(\n                rect = RectF(0.01F, 0.00F, 0.01F, 0.00F),\n                confidence = 1F,\n                label = 1,\n            ),\n            DetectionBox(\n                rect = RectF(0.02F, 0.00F, 0.02F, 0.00F),\n                confidence = 1F,\n                label = 2,\n            ),\n            DetectionBox(\n                rect = RectF(0.03F, 0.00F, 0.03F, 0.00F),\n                confidence = 1F,\n                label = 3,\n            ),\n            DetectionBox(\n                rect = RectF(0.00F, 0.25F, 0.00F, 0.25F),\n                confidence = 1F,\n                label = 4,\n            ),\n            DetectionBox(\n                rect = RectF(0.01F, 0.25F, 0.01F, 0.25F),\n                confidence = 1F,\n                label = 5,\n            ),\n            DetectionBox(\n                rect = RectF(0.02F, 0.25F, 0.02F, 0.25F),\n                confidence = 1F,\n                label = 6,\n            ),\n            DetectionBox(\n                rect = RectF(0.03F, 0.25F, 0.04F, 0.25F),\n                confidence = 1F,\n                label = 7,\n            ),\n            DetectionBox(\n                rect = RectF(0.00F, 0.50F, 0.00F, 0.50F),\n                confidence = 1F,\n                label = 8,\n            ),\n            DetectionBox(\n                rect = RectF(0.01F, 0.50F, 0.01F, 0.50F),\n                confidence = 1F,\n                label = 9,\n            ),\n            DetectionBox(\n                rect = RectF(0.02F, 0.50F, 0.02F, 0.50F),\n                confidence = 1F,\n                label = 0,\n            ),\n            DetectionBox(\n                rect = RectF(0.03F, 0.50F, 0.03F, 0.50F),\n                confidence = 1F,\n                label = 1,\n            ),\n            DetectionBox(\n                rect = RectF(0.00F, 0.75F, 0.00F, 0.75F),\n                confidence = 1F,\n                label = 2,\n            ),\n            DetectionBox(\n                rect = RectF(0.01F, 0.75F, 0.01F, 0.75F),\n                confidence = 1F,\n                label = 3,\n            ),\n            DetectionBox(\n                rect = RectF(0.02F, 0.75F, 0.02F, 0.75F),\n                confidence = 1F,\n                label = 4,\n            ),\n            DetectionBox(\n                rect = RectF(0.03F, 0.75F, 0.03F, 0.75F),\n                confidence = 1F,\n                label = 5,\n            ),\n        )\n\n        assertEquals(\"0123456789012345\", determineLayoutAndFilter(numbers, VERTICAL_THRESHOLD).map { it.label }.joinToString(\"\"))\n    }\n\n    @Test\n    fun determineLayoutAndFilter_linearDiagonalTopLeftBottomRight() {\n        val numbers = listOf(\n            DetectionBox(\n                rect = RectF(0.00F, 0.00F, 0.00F, 0.40F),\n                confidence = 1F,\n                label = 0,\n            ),\n            DetectionBox(\n                rect = RectF(0.01F, 0.04F, 0.01F, 0.44F),\n                confidence = 1F,\n                label = 1,\n            ),\n            DetectionBox(\n                rect = RectF(0.02F, 0.08F, 0.02F, 0.48F),\n                confidence = 1F,\n                label = 2,\n            ),\n            DetectionBox(\n                rect = RectF(0.03F, 0.12F, 0.03F, 0.52F),\n                confidence = 1F,\n                label = 3,\n            ),\n            DetectionBox(\n                rect = RectF(0.04F, 0.16F, 0.04F, 0.56F),\n                confidence = 1F,\n                label = 4,\n            ),\n            DetectionBox(\n                rect = RectF(0.05F, 0.20F, 0.05F, 0.60F),\n                confidence = 1F,\n                label = 5,\n            ),\n            DetectionBox(\n                rect = RectF(0.06F, 0.24F, 0.06F, 0.64F),\n                confidence = 1F,\n                label = 6,\n            ),\n            DetectionBox(\n                rect = RectF(0.07F, 0.28F, 0.07F, 0.68F),\n                confidence = 1F,\n                label = 7,\n            ),\n            DetectionBox(\n                rect = RectF(0.08F, 0.32F, 0.08F, 0.72F),\n                confidence = 1F,\n                label = 8,\n            ),\n            DetectionBox(\n                rect = RectF(0.09F, 0.36F, 0.09F, 0.76F),\n                confidence = 1F,\n                label = 9,\n            ),\n            DetectionBox(\n                rect = RectF(0.10F, 0.40F, 0.10F, 0.80F),\n                confidence = 1F,\n                label = 0,\n            ),\n            DetectionBox(\n                rect = RectF(0.11F, 0.44F, 0.11F, 0.84F),\n                confidence = 1F,\n                label = 1,\n            ),\n            DetectionBox(\n                rect = RectF(0.12F, 0.48F, 0.12F, 0.88F),\n                confidence = 1F,\n                label = 2,\n            ),\n            DetectionBox(\n                rect = RectF(0.13F, 0.52F, 0.13F, 0.92F),\n                confidence = 1F,\n                label = 3,\n            ),\n            DetectionBox(\n                rect = RectF(0.14F, 0.56F, 0.14F, 0.96F),\n                confidence = 1F,\n                label = 4,\n            ),\n            DetectionBox(\n                rect = RectF(0.15F, 0.60F, 0.15F, 1.00F),\n                confidence = 1F,\n                label = 5,\n            ),\n        )\n\n        assertEquals(\"0123456789012345\", determineLayoutAndFilter(numbers, VERTICAL_THRESHOLD).map { it.label }.joinToString(\"\"))\n    }\n\n    @Test\n    fun determineLayoutAndFilter_linearDiagonalBottomLeftTopRight() {\n        val numbers = listOf(\n            DetectionBox(\n                rect = RectF(0.00F, 0.60F, 0.00F, 1.00F),\n                confidence = 1F,\n                label = 0,\n            ),\n            DetectionBox(\n                rect = RectF(0.01F, 0.56F, 0.01F, 0.96F),\n                confidence = 1F,\n                label = 1,\n            ),\n            DetectionBox(\n                rect = RectF(0.02F, 0.52F, 0.02F, 0.92F),\n                confidence = 1F,\n                label = 2,\n            ),\n            DetectionBox(\n                rect = RectF(0.03F, 0.48F, 0.03F, 0.88F),\n                confidence = 1F,\n                label = 3,\n            ),\n            DetectionBox(\n                rect = RectF(0.04F, 0.44F, 0.04F, 0.84F),\n                confidence = 1F,\n                label = 4,\n            ),\n            DetectionBox(\n                rect = RectF(0.05F, 0.40F, 0.05F, 0.80F),\n                confidence = 1F,\n                label = 5,\n            ),\n            DetectionBox(\n                rect = RectF(0.06F, 0.36F, 0.06F, 0.76F),\n                confidence = 1F,\n                label = 6,\n            ),\n            DetectionBox(\n                rect = RectF(0.07F, 0.32F, 0.07F, 0.72F),\n                confidence = 1F,\n                label = 7,\n            ),\n            DetectionBox(\n                rect = RectF(0.08F, 0.28F, 0.08F, 0.68F),\n                confidence = 1F,\n                label = 8,\n            ),\n            DetectionBox(\n                rect = RectF(0.09F, 0.24F, 0.09F, 0.64F),\n                confidence = 1F,\n                label = 9,\n            ),\n            DetectionBox(\n                rect = RectF(0.10F, 0.20F, 0.10F, 0.60F),\n                confidence = 1F,\n                label = 0,\n            ),\n            DetectionBox(\n                rect = RectF(0.11F, 0.16F, 0.11F, 0.56F),\n                confidence = 1F,\n                label = 1,\n            ),\n            DetectionBox(\n                rect = RectF(0.12F, 0.12F, 0.12F, 0.52F),\n                confidence = 1F,\n                label = 2,\n            ),\n            DetectionBox(\n                rect = RectF(0.13F, 0.08F, 0.13F, 0.48F),\n                confidence = 1F,\n                label = 3,\n            ),\n            DetectionBox(\n                rect = RectF(0.14F, 0.04F, 0.14F, 0.44F),\n                confidence = 1F,\n                label = 4,\n            ),\n            DetectionBox(\n                rect = RectF(0.15F, 0.00F, 0.15F, 0.40F),\n                confidence = 1F,\n                label = 5,\n            ),\n        )\n\n        assertEquals(\"0123456789012345\", determineLayoutAndFilter(numbers, VERTICAL_THRESHOLD).map { it.label }.joinToString(\"\"))\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.getbouncer.scan.payment\" />\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/FrameDetails.kt",
    "content": "package com.getbouncer.scan.payment\n\nimport androidx.annotation.Keep\n\n@Keep\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class FrameDetails(\n    val panSideConfidence: Float,\n    val noPanSideConfidence: Float,\n    val noCardConfidence: Float,\n    val hasPan: Boolean,\n)\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/Image.kt",
    "content": "package com.getbouncer.scan.payment\n\nimport android.app.ActivityManager\nimport android.content.Context\nimport android.content.pm.ConfigurationInfo\nimport android.graphics.Bitmap\nimport android.graphics.Rect\nimport android.os.Build\nimport android.util.Log\nimport android.util.Size\nimport androidx.annotation.CheckResult\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.image.crop\nimport com.getbouncer.scan.framework.image.size\nimport com.getbouncer.scan.framework.util.centerOn\nimport com.getbouncer.scan.framework.util.intersectionWith\nimport com.getbouncer.scan.framework.util.maxAspectRatioInSize\nimport com.getbouncer.scan.framework.util.projectRegionOfInterest\nimport com.getbouncer.scan.framework.util.toRect\n\n/**\n * Get a rect indicating what part of the preview is actually visible on screen. This assumes that the preview\n * is the same size or larger than the screen in both dimensions.\n */\nprivate fun getVisiblePreview(previewBounds: Rect) = Size(\n    previewBounds.right + previewBounds.left,\n    previewBounds.bottom + previewBounds.top,\n)\n\n/**\n * Crop the preview image from the camera based on the view finder's position in the preview bounds.\n *\n * Note: This algorithm makes some assumptions:\n * 1. the previewBounds and the cameraPreviewImage are centered relative to each other.\n * 2. the previewBounds circumscribes the cameraPreviewImage. I.E. they share at least one field of view, and the\n *    cameraPreviewImage's fields of view are smaller than or the same size as the previewBounds's\n * 3. the previewBounds and the cameraPreviewImage have the same orientation\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun cropCameraPreviewToViewFinder(\n    cameraPreviewImage: Bitmap,\n    previewBounds: Rect,\n    viewFinder: Rect,\n): Bitmap {\n    require(\n        viewFinder.left >= previewBounds.left &&\n            viewFinder.right <= previewBounds.right &&\n            viewFinder.top >= previewBounds.top &&\n            viewFinder.bottom <= previewBounds.bottom\n    ) { \"View finder $viewFinder is outside preview image bounds $previewBounds\" }\n\n    // Scale the cardFinder to match the full image\n    val projectedViewFinder = previewBounds\n        .projectRegionOfInterest(\n            toSize = cameraPreviewImage.size(),\n            regionOfInterest = viewFinder\n        )\n        .intersectionWith(cameraPreviewImage.size().toRect())\n\n    return cameraPreviewImage.crop(projectedViewFinder)\n}\n\n/**\n * Crop the preview image from the camera based on a square surrounding the view finder's position in the preview\n * bounds.\n *\n * Note: This algorithm makes some assumptions:\n * 1. the previewBounds and the cameraPreviewImage are centered relative to each other.\n * 2. the previewBounds circumscribes the cameraPreviewImage. I.E. they share at least one field of view, and the\n *    cameraPreviewImage's fields of view are smaller than or the same size as the previewBounds's\n * 3. the previewBounds and the cameraPreviewImage have the same orientation\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun cropCameraPreviewToSquare(\n    cameraPreviewImage: Bitmap,\n    previewBounds: Rect,\n    viewFinder: Rect,\n): Bitmap {\n    require(\n        viewFinder.left >= previewBounds.left &&\n            viewFinder.right <= previewBounds.right &&\n            viewFinder.top >= previewBounds.top &&\n            viewFinder.bottom <= previewBounds.bottom\n    ) { \"Card finder is outside preview image bounds\" }\n\n    val visiblePreview = getVisiblePreview(previewBounds)\n    val squareViewFinder = maxAspectRatioInSize(visiblePreview, 1F).centerOn(viewFinder)\n\n    // calculate the projected squareViewFinder\n    val projectedSquare = previewBounds\n        .projectRegionOfInterest(cameraPreviewImage.size(), squareViewFinder)\n        .intersectionWith(cameraPreviewImage.size().toRect())\n\n    return cameraPreviewImage.crop(projectedSquare)\n}\n\n/**\n * Determine if the device supports OpenGL version 3.1.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun hasOpenGl31(context: Context): Boolean {\n    val openGlVersion = 0x00030001\n    val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager\n    val configInfo = activityManager.deviceConfigurationInfo\n\n    val isSupported = if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {\n        configInfo.reqGlEsVersion >= openGlVersion && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P\n    } else {\n        false\n    }\n\n    Log.d(Config.logTag, \"OpenGL is supported? $isSupported\")\n    return isSupported\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/ModelManager.kt",
    "content": "package com.getbouncer.scan.payment\n\nimport android.content.Context\nimport androidx.annotation.VisibleForTesting\nimport com.getbouncer.scan.framework.FetchedData\nimport com.getbouncer.scan.framework.Fetcher\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nabstract class ModelManager {\n    private lateinit var fetcher: Fetcher\n    private val fetcherMutex = Mutex()\n\n    private var onFetch: ((success: Boolean) -> Unit)? = null\n\n    suspend fun fetchModel(context: Context, forImmediateUse: Boolean, isOptional: Boolean = false): FetchedData {\n        fetcherMutex.withLock {\n            if (!this::fetcher.isInitialized) {\n                fetcher = getModelFetcher(context.applicationContext)\n            }\n        }\n        return fetcher.fetchData(forImmediateUse, isOptional).also {\n            onFetch?.invoke(it.successfullyFetched)\n        }\n    }\n\n    suspend fun isReady() = fetcherMutex.withLock {\n        if (this::fetcher.isInitialized) fetcher.isCached() else false\n    }\n\n    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)\n    abstract fun getModelFetcher(context: Context): Fetcher\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/TextDetectModelManager.kt",
    "content": "package com.getbouncer.scan.payment\n\nimport android.content.Context\nimport com.getbouncer.scan.framework.Fetcher\nimport com.getbouncer.scan.payment.ml.TextDetect\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nobject TextDetectModelManager : ModelManager() {\n    override fun getModelFetcher(context: Context): Fetcher = TextDetect.ModelFetcher(context)\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/analyzer/NameAndExpiryAnalyzer.kt",
    "content": "package com.getbouncer.scan.payment.analyzer\n\nimport android.graphics.Bitmap\nimport android.graphics.Rect\nimport android.graphics.RectF\nimport android.util.Log\nimport com.getbouncer.scan.framework.Analyzer\nimport com.getbouncer.scan.framework.AnalyzerFactory\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.image.size\nimport com.getbouncer.scan.framework.ml.hardNonMaximumSuppression\nimport com.getbouncer.scan.framework.ml.ssd.rectForm\nimport com.getbouncer.scan.framework.util.centerScaled\nimport com.getbouncer.scan.framework.util.scaled\nimport com.getbouncer.scan.payment.cropCameraPreviewToSquare\nimport com.getbouncer.scan.payment.ml.AlphabetDetect\nimport com.getbouncer.scan.payment.ml.ExpiryDetect\nimport com.getbouncer.scan.payment.ml.TextDetect\nimport com.getbouncer.scan.payment.ml.ssd.DetectionBox\nimport kotlin.math.max\nimport kotlin.math.min\n\n// Some params for how we post process our name detector\n// Number of predictions per predicted character box\nprivate const val NUM_PREDICTION_STRIDES = 10\nprivate const val NMS_THRESHOLD = 0.85F\nprivate const val CHAR_CONFIDENCE_THRESHOLD = 0.5\n\nprivate const val NAME_BOX_X_SCALE_RATIO = 1.2F\nprivate const val NAME_BOX_Y_SCALE_RATIO = 1.4F\n\nprivate const val EXPIRY_BOX_X_SCALE_RATIO = 1.1F\nprivate const val EXPIRY_BOX_Y_SCALE_RATIO = 1.2F\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nclass NameAndExpiryAnalyzer private constructor(\n    private val textDetect: TextDetect?,\n    private val alphabetDetect: AlphabetDetect?,\n    private val expiryDetect: ExpiryDetect?,\n    val runNameExtraction: Boolean,\n    val runExpiryExtraction: Boolean,\n) : Analyzer<NameAndExpiryAnalyzer.Input, Any, NameAndExpiryAnalyzer.Prediction> {\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Input(\n        val cameraPreviewImage: TrackedImage<Bitmap>,\n        val previewBounds: Rect,\n        val cardFinder: Rect,\n    )\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Prediction(\n        val name: String?,\n        val boxes: List<DetectionBox>?,\n        val expiry: ExpiryDetect.Expiry?\n    )\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    fun isExpiryDetectorAvailable() = textDetect != null && expiryDetect != null\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    fun isNameDetectorAvailable() = textDetect != null && alphabetDetect != null\n\n    override suspend fun analyze(\n        data: Input,\n        state: Any\n    ) = if ((!runNameExtraction && !runExpiryExtraction) || textDetect == null) {\n        Prediction(null, null, null)\n    } else {\n        val textDetectorPrediction = textDetect.analyze(\n            TextDetect.cameraPreviewToInput(data.cameraPreviewImage, data.previewBounds, data.cardFinder),\n            Unit,\n        )\n\n        val squareImage = TrackedImage(\n            cropCameraPreviewToSquare(data.cameraPreviewImage.image, data.previewBounds, data.cardFinder),\n            data.cameraPreviewImage.tracker,\n        )\n\n        data.cameraPreviewImage.tracker.trackResult(\"name_and_expiry_image_cropped\")\n\n        val expiry = if (runExpiryExtraction && textDetectorPrediction.expiryBoxes.isNotEmpty()) {\n            // pick the expiry box by oldest date\n            // the boxes produced by textDetector are sometimes too tight, especially in the Y\n            // direction. Scale it out a bit\n            textDetectorPrediction.expiryBoxes.mapNotNull { box ->\n                expiryDetect?.analyze(\n                    ExpiryDetect.cameraPreviewToInput(\n                        data.cameraPreviewImage,\n                        data.previewBounds,\n                        data.cardFinder,\n                        // the boxes produced by textDetector are sometimes too tight, especially in the Y\n                        // direction. Scale it out a bit\n                        box.rect.centerScaled(\n                            EXPIRY_BOX_X_SCALE_RATIO,\n                            EXPIRY_BOX_Y_SCALE_RATIO\n                        ),\n                    ),\n                    Unit\n                )?.expiry\n            }.maxOrNull()\n        } else {\n            null\n        }\n\n        val name = if (runNameExtraction) {\n            textDetectorPrediction.nameBoxes.mapNotNull { box ->\n                // the boxes produced by textDetector are sometimes too tight, especially in the Y\n                // direction. Scale it out a bit\n                processNamePredictions(\n                    box.rect.centerScaled(\n                        NAME_BOX_X_SCALE_RATIO,\n                        NAME_BOX_Y_SCALE_RATIO,\n                    ),\n                    squareImage,\n                )?.filter { it != ' ' }\n            }.joinToString(\" \").trim().ifEmpty { null }\n        } else {\n            null\n        }\n\n        Prediction(name, textDetectorPrediction.allObjects, expiry)\n    }\n\n    private data class CharPredictionWithBox(val characterPrediction: AlphabetDetect.Prediction, val box: RectF) {\n        fun getNormalizedRectForm(width: Int, height: Int) = rectForm(\n            left = box.left / width,\n            top = box.top / height,\n            right = box.right / width,\n            bottom = box.bottom / height\n        )\n    }\n\n    private suspend fun processNamePredictions(\n        nameRect: RectF,\n        squareImage: TrackedImage<Bitmap>\n    ): String? {\n        if (alphabetDetect == null) {\n            return null\n        }\n\n        val scaledNameRect = nameRect.scaled(squareImage.image.size())\n        val x = scaledNameRect.left.toInt()\n        val y = scaledNameRect.top.toInt()\n        val width = scaledNameRect.width().toInt()\n        val height = scaledNameRect.height().toInt()\n\n        // We use a square aspect ratio for our character recognizer, so we assume that the height\n        // of the name bounding box is the height and width of the square\n        val charWidth = height\n\n        // We adjust the start and end of the name bounding box to better capture the first char\n        val xStart = max(0, x - charWidth / 4)\n        val nameWidth = min(squareImage.image.width - xStart, width + charWidth / 2)\n\n        if (y < 0 || height < 0 || y + height > squareImage.image.height || xStart + nameWidth > squareImage.image.width) {\n            Log.w(Config.logTag, \"Invalid name dimensions. height=$height, y=$y\")\n            return null\n        }\n\n        val nameBitmap = Bitmap.createBitmap(squareImage.image, xStart, y, nameWidth, height)\n        val predictions: MutableList<CharPredictionWithBox> = ArrayList()\n\n        // iterate through each stride, making a prediction per stride\n        var nameX = 0\n        while (nameX < nameWidth - charWidth) {\n            val firstLetterBitmap = TrackedImage(\n                image = Bitmap.createBitmap(nameBitmap, nameX, 0, height, height),\n                tracker = squareImage.tracker,\n            )\n            predictions.add(\n                CharPredictionWithBox(\n                    characterPrediction = alphabetDetect.analyze(AlphabetDetect.Input(firstLetterBitmap), Unit),\n                    box = RectF(nameX.toFloat(), 0F, height.toFloat(), height.toFloat())\n                )\n            )\n            nameX += charWidth / NUM_PREDICTION_STRIDES\n        }\n\n        val (boxes, probabilities) = predictions.map {\n            it.getNormalizedRectForm(\n                width = squareImage.image.width,\n                height = squareImage.image.height,\n            ) to it.characterPrediction.confidence\n        }.unzip()\n\n        val indices: List<Int> = hardNonMaximumSuppression(\n            boxes.toTypedArray(),\n            probabilities.toFloatArray(),\n            NMS_THRESHOLD,\n            limit = 0\n        )\n\n        return processNMSResults(predictions.filterIndexed { index, _ -> indices.contains(index) })\n    }\n\n    /**\n     * Processes each cluster of letters from NMS, doing a simple voting algorithm and\n     * tie-breaking with confidence\n     */\n    private fun processNMSCluster(charClusters: List<AlphabetDetect.Prediction>): Char {\n        var candidateLetter = 0.toChar()\n        var candidateLetterConfidence = 0f\n        var candidateConsecutiveCount = 0\n        var currentConsecutiveCount = 0\n        var currentLetterMaxConfidence = 0f\n        var lastSeenLetter = 0.toChar()\n\n        // This should be using charClusters.forEach, but doing so seems to require API 24. It's unclear why this won't\n        // use the kotlin.collections version of `forEach`, but it's not during compile.\n        for (characterPrediction in charClusters) {\n            if (lastSeenLetter == characterPrediction.character) {\n                currentConsecutiveCount += 1\n                currentLetterMaxConfidence = max(currentLetterMaxConfidence, characterPrediction.confidence)\n            } else {\n                currentConsecutiveCount = 1\n                currentLetterMaxConfidence = characterPrediction.confidence\n                lastSeenLetter = characterPrediction.character\n            }\n\n            if (currentConsecutiveCount == candidateConsecutiveCount && currentLetterMaxConfidence > candidateLetterConfidence ||\n                currentConsecutiveCount > candidateConsecutiveCount\n            ) {\n                candidateLetterConfidence = currentLetterMaxConfidence\n                candidateLetter = characterPrediction.character\n                candidateConsecutiveCount = currentConsecutiveCount\n            }\n        }\n\n        return if (candidateLetterConfidence > CHAR_CONFIDENCE_THRESHOLD) candidateLetter else ' '\n    }\n\n    /**\n     * Black magic to calculate the \"width\" (in terms of prediction index) for each space\n     *   Compares the two widest (by index) spaces with the p25 of all of the background\n     *   predictions, then, checks to see if difference between the widest and the second\n     *   widest is sufficiently close compared to the p25\n     * For example: [1,1,1,1,1,5,6] would yield 5, since p25 is 1, pMax is 6, pMax2 is 5,\n     * and pMax - pMax2 << pMax2 - p25\n     */\n    private fun getSpacesWidth(spaces: List<Int>): Int {\n        if (spaces.size <= 2) {\n            return 10\n        }\n        val slice = spaces.subList(1, spaces.size - 1).sorted()\n        val p25Index = slice.size * 25 / 100\n        val p25 = slice[p25Index]\n        val pmax = slice[slice.size - 1]\n        val pmax2 = if (slice.size >= 2) slice[slice.size - 2] else pmax\n\n        return when {\n            pmax == pmax2 && pmax == p25 -> pmax + 1\n            (pmax - pmax2) * 2 <= pmax2 - p25 -> pmax2\n            else -> pmax\n        }\n    }\n\n    /**\n     * Accepts the output from hard NMS and produces the predicted word\n     */\n    private fun processNMSResults(\n        predictions: List<CharPredictionWithBox>\n    ): String? {\n        val intermediateChars: MutableList<Char> = ArrayList()\n        val charClusters: MutableList<AlphabetDetect.Prediction> = ArrayList()\n        val spaces: MutableList<Int> = ArrayList()\n\n        var currentSpaceClusterSize = 0\n        val debugWord = StringBuilder()\n        for (prediction in predictions) {\n            debugWord.append(prediction.characterPrediction.character)\n            if (prediction.characterPrediction.character == ' ') {\n                if (charClusters.size > 0) {\n                    // process the cluster\n                    intermediateChars.add(processNMSCluster(charClusters))\n                    charClusters.clear()\n                }\n                currentSpaceClusterSize += 1\n                intermediateChars.add(' ')\n            } else {\n                charClusters.add(prediction.characterPrediction)\n                if (currentSpaceClusterSize > 0) {\n                    // add space cluster, reset counter\n                    spaces.add(currentSpaceClusterSize)\n                    currentSpaceClusterSize = 0\n                }\n            }\n        }\n\n        // do one last process if we ended w/ a char\n        if (charClusters.size > 0) {\n            // process the cluster\n            intermediateChars.add(processNMSCluster(charClusters))\n            charClusters.clear()\n        }\n\n        val word = StringBuilder()\n        val spaceWidth = getSpacesWidth(spaces)\n        var numConsecSpaces = 0\n        for (c in intermediateChars) {\n            if (c == ' ') {\n                numConsecSpaces += 1\n                if (numConsecSpaces == spaceWidth) {\n                    word.append(' ')\n                }\n            } else {\n                word.append(c)\n                numConsecSpaces = 0\n            }\n        }\n\n        return word.toString().trim { it <= ' ' }\n    }\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    class Factory(\n        private val textDetectFactory: TextDetect.Factory,\n        private val alphabetDetectFactory: AlphabetDetect.Factory? = null,\n        private val expiryDetectFactory: ExpiryDetect.Factory? = null,\n        private val runNameExtraction: Boolean,\n        private val runExpiryExtraction: Boolean,\n    ) : AnalyzerFactory<Input, Any, Prediction, NameAndExpiryAnalyzer> {\n        override suspend fun newInstance() = NameAndExpiryAnalyzer(\n            textDetect = textDetectFactory.newInstance(),\n            alphabetDetect = alphabetDetectFactory?.newInstance(),\n            expiryDetect = expiryDetectFactory?.newInstance(),\n            runNameExtraction = runNameExtraction,\n            runExpiryExtraction = runExpiryExtraction,\n        )\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/card/CardExpiry.kt",
    "content": "package com.getbouncer.scan.payment.card\n\nimport java.util.Calendar\n\n/**\n * Determine if the given expiry is currently valid.\n *\n * Validity is determined by the following logic:\n * 1. Determine if the expiry year is after the current year. If after, return true.\n * 2. If the year is the same as the current year, determine if the month is after the current\n *    month. If after, return true.\n * 3. If the month is the same as the current month, determine if the day is after the current day.\n *    If after or the current day, return true.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun isValidExpiry(day: String?, month: String, year: String): Boolean {\n    val calendar = Calendar.getInstance()\n\n    val cardYear = getFourDigitYear(year, calendar)\n    val currentYear = getCurrentYear(calendar)\n\n    val currentMonth = calendar.get(Calendar.MONTH) + 1 // months are 0-based in calendar.\n    val cardMonth = formatExpiryMonth(month).toIntOrNull() ?: 0\n    if (!isValidMonth(cardMonth)) {\n        return false\n    }\n\n    val currentDay = calendar.get(Calendar.DAY_OF_MONTH)\n    val cardDay = day?.toIntOrNull() ?: 31\n    if (day != null && !isValidDay(cardDay, cardMonth, cardYear)) {\n        return false\n    }\n\n    // according to https://stackoverflow.com/questions/2500588/maximum-year-in-expiry-date-of-credit-card,\n    // it's possible to have expires up to 50 years from now.\n    if (cardYear > currentYear && cardYear < currentYear + 100) {\n        return true\n    } else if (cardYear < currentYear) {\n        return false\n    }\n\n    if (cardMonth > currentMonth) {\n        return true\n    } else if (cardMonth < currentMonth) {\n        return false\n    }\n\n    return cardDay >= currentDay\n}\n\n/**\n * Format the card expiry as a human readable format.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun formatExpiry(day: String?, month: String, year: String): String {\n    val formattedDay = if (day != null) \"${formatExpiryDay(day)}/\" else \"\"\n    return \"$formattedDay${formatExpiryMonth(month)}/${formatExpiryYear(year)}\"\n}\n\n/**\n * Determine if a month string is valid.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun isValidMonth(month: String) = month.toIntOrNull()?.let { isValidMonth(it) } ?: false\n\n/**\n * Determine if a month integer is valid.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun isValidMonth(month: Int) = month in 1..12\n\n/**\n * Determine if the day\n */\nprivate fun isValidDay(day: Int, month: Int, year: Int): Boolean {\n    val calendar = Calendar.getInstance()\n    calendar.set(year, month - 1, 1)\n    return day >= 1 && day <= calendar.getActualMaximum(Calendar.DAY_OF_MONTH)\n}\n\n/**\n * Convert the year string to four digits.\n */\nprivate fun getFourDigitYear(year: String, calendar: Calendar): Int = year.digitsOnly().let {\n    when {\n        it.length == 4 -> it.toIntOrNull()\n        it.length > 4 -> it.takeLast(4).toIntOrNull()\n        else -> (getCurrentCentury(calendar) + it.padStart(2, '0').takeLast(2)).toIntOrNull()\n    } ?: 0\n}\n\n/**\n * Get the current century\n */\nprivate fun getCurrentCentury(calendar: Calendar): String =\n    getCurrentYear(calendar).toString().take(2)\n\n/**\n * Get the current year.\n */\nprivate fun getCurrentYear(calendar: Calendar): Int = calendar.get(Calendar.YEAR)\n\n/**\n * Format the expiry day. If the input is null, this returns null.\n */\nprivate fun formatExpiryDay(day: String?) = day?.padStart(2, '0')?.take(2)\n\n/**\n * Format the expiry month as a two-digit number.\n */\nprivate fun formatExpiryMonth(month: String) = month.padStart(2, '0').take(2)\n\n/**\n * Format the expiry year as a two-digit number.\n */\nprivate fun formatExpiryYear(year: String) = year.padStart(2, '0').takeLast(2)\n\n/**\n * Remove all non-digit characters from a string.\n */\nprivate fun String?.digitsOnly(): String = (this?.filter { it.isDigit() } ?: \"\")\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/card/CardIssuer.kt",
    "content": "package com.getbouncer.scan.payment.card\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\n\n/**\n * A list of supported card issuers.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsealed class CardIssuer(open val displayName: String) : Parcelable {\n    @Parcelize\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    object AmericanExpress : CardIssuer(\"American Express\")\n\n    @Parcelize\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Custom(override val displayName: String) : CardIssuer(displayName)\n\n    @Parcelize\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    object DinersClub : CardIssuer(\"Diners Club\")\n\n    @Parcelize\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    object Discover : CardIssuer(\"Discover\")\n\n    @Parcelize\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    object JCB : CardIssuer(\"JCB\")\n\n    @Parcelize\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    object MasterCard : CardIssuer(\"MasterCard\")\n\n    @Parcelize\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    object UnionPay : CardIssuer(\"UnionPay\")\n\n    @Parcelize\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    object Unknown : CardIssuer(\"Unknown\")\n\n    @Parcelize\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    object Visa : CardIssuer(\"Visa\")\n}\n\n/**\n * Format the card network as a human readable format.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun formatIssuer(issuer: CardIssuer): String = issuer.displayName\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/card/CardType.kt",
    "content": "package com.getbouncer.scan.payment.card\n\nimport android.content.Context\nimport androidx.annotation.CheckResult\nimport androidx.annotation.RawRes\nimport com.getbouncer.scan.framework.util.cacheFirstResultSuspend\nimport com.getbouncer.scan.payment.R\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport java.io.ByteArrayInputStream\nimport java.io.FileInputStream\nimport java.nio.ByteBuffer\nimport java.util.zip.ZipInputStream\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsealed class CardType {\n    object Credit : CardType()\n    object Debit : CardType()\n    object Prepaid : CardType()\n    object Unknown : CardType()\n}\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nval getTypeTable: suspend (Context) -> Map<IntRange, CardType> = cacheFirstResultSuspend { context: Context ->\n    readRawZippedResourceToStringArray(context, R.raw.payment_card_types).map {\n        val fields = it.split(\",\")\n        fields[0].toInt()..fields[1].toInt() to when (fields[2]) {\n            \"CREDIT\" -> CardType.Credit\n            \"DEBIT\" -> CardType.Debit\n            \"PREPAID\" -> CardType.Prepaid\n            else -> CardType.Unknown\n        }\n    }.toMap()\n}\n\n/**\n * Read a raw resource into a [ByteBuffer].\n */\n@CheckResult\nprivate suspend fun readRawZippedResourceToStringArray(context: Context, @RawRes resourceId: Int) =\n    withContext(Dispatchers.IO) {\n        val byteArray = context.resources.openRawResourceFd(resourceId).use { fileDescriptor ->\n            val byteBuffer = ByteBuffer.allocate(fileDescriptor.declaredLength.toInt())\n            FileInputStream(fileDescriptor.fileDescriptor).use { fileInputStream ->\n                fileInputStream.channel.read(byteBuffer, fileDescriptor.startOffset)\n                byteBuffer.rewind()\n                byteBuffer.array()\n            }\n        }\n\n        ByteArrayInputStream(byteArray).use { byteArrayInputStream ->\n            ZipInputStream(byteArrayInputStream).use { zipInputStream ->\n                var entry = zipInputStream.nextEntry\n                while (entry != null && !entry.name.endsWith(\".txt\")) {\n                    entry = zipInputStream.nextEntry\n                }\n                if (entry != null) {\n                    zipInputStream.bufferedReader().readLines()\n                } else {\n                    emptyList()\n                }\n            }\n        }\n    }\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/card/PanFormatter.kt",
    "content": "package com.getbouncer.scan.payment.card\n\n/*\n * The following are known PAN formats. The information in this table was taken from\n * https://baymard.com/checkout-usability/credit-card-patterns and indirectly from\n * https://web.archive.org/web/20170822221741/https://www.discovernetwork.com/downloads/IPP_VAR_Compliance.pdf\n *\n * | ------------------------- | ------- | ------------------------------------ |\n * | Issuer                    | PAN Len | Display Format                       |\n * | ------------------------- | ------- | ------------------------------------ |\n * | American Express          | 15      | 4 - 6 - 5                            |\n * | Diners Club International | 14      | 4 - 6 - 4                            |\n * | Diners Club International | 15      | Unknown                              |\n * | Diners Club International | 16      | 4 - 4 - 4 - 4                        |\n * | Diners Club International | 17      | Unknown                              |\n * | Diners Club International | 18      | Unknown                              |\n * | Diners Club International | 19      | Unknown                              |\n * | Discover                  | 16      | 4 - 4 - 4 - 4                        |\n * | Discover                  | 17      | Unknown                              |\n * | Discover                  | 18      | Unknown                              |\n * | Discover                  | 19      | Unknown                              |\n * | MasterCard                | 16      | 4 - 4 - 4 - 4                        |\n * | MasterCard (Maestro)      | 12      | Unknown                              |\n * | MasterCard (Maestro)      | 13      | 4 - 4 - 5                            |\n * | MasterCard (Maestro)      | 14      | Unknown                              |\n * | MasterCard (Maestro)      | 15      | 4 - 6 - 5                            |\n * | MasterCard (Maestro)      | 16      | 4 - 4 - 4 - 4                        |\n * | MasterCard (Maestro)      | 17      | Unknown                              |\n * | MasterCard (Maestro)      | 18      | Unknown                              |\n * | MasterCard (Maestro)      | 19      | 4 - 4 - 4 - 4 - 3                    |\n * | UnionPay                  | 16      | 4 - 4 - 4 - 4                        |\n * | UnionPay                  | 17      | Unknown                              |\n * | UnionPay                  | 18      | Unknown                              |\n * | UnionPay                  | 19      | 6 - 13                               |\n * | Visa                      | 16      | 4 - 4 - 4 - 4                        |\n * | ------------------------- | ------- | ------------------------------------ |\n */\n\n/**\n * Format a card PAN for display.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun formatPan(pan: String) = normalizeCardNumber(pan).let {\n    val issuer = getCardIssuer(pan)\n    val formatter = CUSTOM_PAN_FORMAT_TABLE[issuer]?.get(pan.length)\n        ?: PAN_FORMAT_TABLE[issuer]?.get(pan.length) ?: DEFAULT_PAN_FORMATTERS[pan.length]\n    formatter?.formatPan(pan) ?: pan\n}\n\n/**\n * Add a new way to format a PAN\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun addFormatPan(cardIssuer: CardIssuer, length: Int, vararg blockSizes: Int) {\n    CUSTOM_PAN_FORMAT_TABLE.getOrPut(cardIssuer, { mutableMapOf<Int, PanFormatter>() })[length] =\n        PanFormatter(*blockSizes)\n}\n\n/**\n * A class that can format a PAN for display given a list of number block sizes.\n */\nprivate class PanFormatter(vararg blockSizes: Int) {\n    private val blockIndices = blockSizesToIndicies(blockSizes)\n\n    private fun blockSizesToIndicies(blockSizes: IntArray): List<Int> {\n        var currentIndex = 0\n        return blockSizes.map {\n            val newValue = it + currentIndex\n            currentIndex = newValue\n            newValue\n        }\n    }\n\n    /**\n     * Format the PAN for display using the number block sizes.\n     */\n    fun formatPan(pan: String): String {\n        val builder = StringBuilder()\n        for (i in pan.indices) {\n            if (i in blockIndices) {\n                builder.append(' ')\n            }\n            builder.append(pan[i])\n        }\n\n        return builder.toString()\n    }\n}\n\n/**\n * A mapping of [CardIssuer] to length and [PanFormatter]\n */\nprivate val PAN_FORMAT_TABLE: Map<CardIssuer, Map<Int, PanFormatter>> = mapOf(\n    CardIssuer.AmericanExpress to mapOf(\n        15 to PanFormatter(4, 6, 5)\n    ),\n\n    CardIssuer.DinersClub to mapOf(\n        14 to PanFormatter(4, 6, 4),\n        15 to PanFormatter(4, 6, 5), // Best guess\n        16 to PanFormatter(4, 4, 4, 4),\n        17 to PanFormatter(4, 4, 4, 5), // Best guess\n        18 to PanFormatter(4, 4, 4, 6), // Best guess\n        19 to PanFormatter(4, 4, 4, 4, 3) // Best guess\n    ),\n\n    CardIssuer.Discover to mapOf(\n        16 to PanFormatter(4, 4, 4, 4),\n        17 to PanFormatter(4, 4, 4, 4, 1), // Best guess\n        18 to PanFormatter(4, 4, 4, 4, 2), // Best guess\n        19 to PanFormatter(4, 4, 4, 4, 3) // Best guess\n    ),\n\n    CardIssuer.MasterCard to mapOf(\n        12 to PanFormatter(4, 4, 4), // Best guess\n        13 to PanFormatter(4, 4, 5),\n        14 to PanFormatter(4, 6, 4), // Best guess\n        15 to PanFormatter(4, 6, 5),\n        16 to PanFormatter(4, 4, 4, 4),\n        17 to PanFormatter(4, 4, 4, 5), // Best guess\n        18 to PanFormatter(4, 4, 4, 6), // Best guess\n        19 to PanFormatter(4, 4, 4, 4, 3) // Best guess\n    ),\n\n    CardIssuer.UnionPay to mapOf(\n        16 to PanFormatter(4, 4, 4, 4),\n        17 to PanFormatter(4, 4, 4, 5), // Best guess\n        18 to PanFormatter(4, 4, 4, 6), // Best guess\n        19 to PanFormatter(6, 13)\n    ),\n\n    CardIssuer.Visa to mapOf(\n        16 to PanFormatter(4, 4, 4, 4)\n    )\n)\n\n/**\n * Default length [PanFormatter] mappings.\n */\nprivate val DEFAULT_PAN_FORMATTERS: Map<Int, PanFormatter> = mapOf(\n    12 to PanFormatter(4, 4, 4),\n    13 to PanFormatter(4, 4, 5),\n    14 to PanFormatter(4, 6, 4),\n    15 to PanFormatter(4, 6, 5),\n    16 to PanFormatter(4, 4, 4, 4),\n    17 to PanFormatter(4, 4, 4, 5),\n    18 to PanFormatter(4, 4, 4, 2),\n    19 to PanFormatter(4, 4, 4, 4, 3)\n)\n\n/**\n * A mapping of [CardIssuer] to length and [PanFormatter]\n */\nprivate val CUSTOM_PAN_FORMAT_TABLE: MutableMap<CardIssuer, MutableMap<Int, PanFormatter>> = mutableMapOf()\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/card/PanValidator.kt",
    "content": "package com.getbouncer.scan.payment.card\n\n/**\n * A class that provides a method to determine if a PAN is valid.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ninterface PanValidator {\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    fun isValidPan(pan: String): Boolean\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    operator fun plus(other: PanValidator): PanValidator = CompositePanValidator(this, other)\n}\n\n/**\n * A [PanValidator] comprised of two separate validators.\n */\nprivate class CompositePanValidator(\n    private val validator1: PanValidator,\n    private val validator2: PanValidator\n) : PanValidator {\n    override fun isValidPan(pan: String): Boolean =\n        validator1.isValidPan(pan) && validator2.isValidPan(pan)\n}\n\n/**\n * A [PanValidator] that ensures the PAN is of a valid length.\n */\ninternal object LengthPanValidator : PanValidator {\n    override fun isValidPan(pan: String): Boolean {\n        val iinData = getIssuerData(pan) ?: return false\n        return pan.length in iinData.panLengths\n    }\n}\n\n/**\n * A [PanValidator] that performs the Luhn check for validation.\n *\n * see https://en.wikipedia.org/wiki/Luhn_algorithm\n */\ninternal object LuhnPanValidator : PanValidator {\n    override fun isValidPan(pan: String): Boolean {\n        if (pan.isEmpty()) {\n            return false\n        }\n\n        fun doubleDigit(digit: Int) = if (digit * 2 > 9) digit * 2 - 9 else digit * 2\n\n        var sum = pan.takeLast(1).toInt()\n        val parity = pan.length % 2\n\n        for (i in 0 until pan.length - 1) {\n            sum += if (i % 2 == parity) {\n                doubleDigit(Character.getNumericValue(pan[i]))\n            } else {\n                Character.getNumericValue(pan[i])\n            }\n        }\n\n        return sum % 10 == 0\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/card/PaymentCard.kt",
    "content": "package com.getbouncer.scan.payment.card\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class PaymentCard(\n    val pan: String?,\n    val expiry: PaymentCardExpiry?,\n    val issuer: CardIssuer?,\n    val cvc: String?,\n    val legalName: String?\n) : Parcelable\n\n@Parcelize\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class PaymentCardExpiry(val day: String?, val month: String, val year: String) : Parcelable\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/card/PaymentCardUtils.kt",
    "content": "@file:JvmName(\"PaymentCardUtils\")\npackage com.getbouncer.scan.payment.card\n\nimport android.content.Context\nimport androidx.annotation.CheckResult\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\n/*\n * Payment cards always have a PAN (Primary Account Number) on one side of the card. This PAN\n * contains identification information about the card itself.\n *\n * The PAN consists of the following:\n *\n * IIN / BIN       Check Digit\n *    |              |\n * |-----|           |\n * 4833 1200 3412 3456\n * |      |---------|\n * |           |\n * MII      Account Number\n *\n * # MII\n * The first digit in a card PAN is the MII (Major Industry Identifier). The following table matches\n * the MII to an industry (taken from https://chargebacks911.com/):\n *\n * | --------------- | --------------------------------------------------------------------------- |\n * | MII Digit Value | Issuer Category                                                             |\n * | --------------- | --------------------------------------------------------------------------- |\n * |        0        | ISO/TC 68 Assignment                                                        |\n * |        1        | Airline cards                                                               |\n * |        2        | Airlines cards (and other future industry assignments)                      |\n * |        3        | Travel and Entertainment Cards                                              |\n * |        4        | Banking and Financial Cards                                                 |\n * |        5        | Banking and Financial Cards                                                 |\n * |        6        | Merchandising and Financial Cards                                           |\n * |        7        | Gas Cards, Other Future Industry Assignments                                |\n * |        8        | Healthcare Cards, Telecommunications, Other Future Industry Assignments     |\n * |        9        | For Use by National Standards Bodies                                        |\n * | --------------- | --------------------------------------------------------------------------- |\n *\n * # IIN / BIN\n * Issuer networks can be identified by the IIN (Issuer Identification Number / Bank Identification\n * Number). The IIN consists of the first few numbers (up to 6) of the card PAN. The IIN translates\n * to the following information:\n *\n * 1. The name, address, and phone number of the bank funds will be transferred from\n * 2. The card brand (Visa, Mastercard, American Express, etc.)\n * 3. What type of card it is (debit, credit, prepaid, etc.)\n * 4. What level the card is (black, platinum, business)\n * 5. Whether the issuer is in the same country as the device used in the transaction\n * 6. Whether the address provided by the cardholder matches the one on file\n *\n * The following table maps supported IINs to issuers, taken from wikipedia\n * https://en.wikipedia.org/wiki/Payment_card_number#Issuer_identification_number_(IIN)\n *\n * | --------------- | ------------------------- | ------- | ------- | --------------------------- |\n * | IIN             | Issuer                    | PAN Len | CVC Len | Validation                  |\n * | --------------- | ------------------------- | ------- | ------- | --------------------------- |\n * | 34****          | American Express          | 15      | 3-4     | Luhn                        |\n * | 37****          | American Express          | 15      | 3-4     | Luhn                        |\n * | 300*** - 305*** | Diners Club International | 16 - 19 | 3       | Luhn                        |\n * | 3095**          | Diners Club International | 16 - 19 | 3       | Luhn                        |\n * | 36****          | Diners Club International | 14 - 19 | 3       | Luhn                        |\n * | 38**** - 39**** | Diners Club International | 16 - 19 | 3       | Luhn                        |\n * | 6011**          | Discover                  | 16 - 19 | 3       | Luhn                        |\n * | 622126 - 622925 | Discover                  | 16 - 19 | 3       | Luhn                        |\n * | 624000 - 626999 | Discover                  | 16 - 19 | 3       | Luhn                        |\n * | 628200 - 628899 | Discover                  | 16 - 19 | 3       | Luhn                        |\n * | 64**** - 65**** | Discover                  | 16 - 19 | 3       | Luhn                        |\n * | 3528** - 3589** | JCB                       | 16 - 19 | 3       | Luhn                        |\n * | 2221** - 2720** | MasterCard                | 16      | 3       | Luhn                        |\n * | 51**** - 55**** | MasterCard                | 16      | 3       | Luhn                        |\n * | 50****          | MasterCard (Maestro)      | 12 - 19 | 3       | Luhn                        |\n * | 56**** - 69**** | MasterCard (Maestro)      | 12 - 19 | 3       | Luhn                        |\n * | 6759**          | MasterCard (Maestro)      | 12 - 19 | 3       | Luhn                        |\n * | 676770          | MasterCard (Maestro)      | 12 - 19 | 3       | Luhn                        |\n * | 676774          | MasterCard (Maestro)      | 12 - 19 | 3       | Luhn                        |\n * | 62****          | UnionPay                  | 16 - 19 | 3       | Luhn                        |\n * | 81****          | UnionPay                  | 16 - 19 | 3       | Luhn                        |\n * | 4*****          | Visa                      | 16 - 19 | 3       | Luhn                        |\n * | --------------- | ------------------------- | ------- | ------- | --------------------------- |\n */\n\nprivate const val IIN_LENGTH = 6\nprivate const val LAST_FOUR_LENGTH = 4\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nconst val QUICK_READ_LENGTH = 16\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nconst val QUICK_READ_GROUP_LENGTH = 4\n/**\n * The Jaccard similarity threshold for determining if two numbers are possibly the same.\n */\nprivate const val JACCARD_SIMILARITY_THRESHOLD = 0.5\n\nprivate val VALID_CVC_LENGTHS = 3..4\n\ninternal data class IssuerData(\n    val iinRange: IntRange,\n    val issuer: CardIssuer,\n    val panLengths: List<Int>,\n    val cvcLengths: List<Int>,\n    val panValidator: PanValidator,\n)\n\n/**\n * This list describes the table above. The order of this list indicates priority. Items higher in\n * the list get priority over items lower in the list when selecting by IIN.\n */\nprivate val ISSUER_TABLE: List<IssuerData> = listOf(\n    IssuerData(340000..349999, CardIssuer.AmericanExpress, listOf(15), (3..4).toList(), LengthPanValidator + LuhnPanValidator),\n    IssuerData(370000..379999, CardIssuer.AmericanExpress, listOf(15), (3..4).toList(), LengthPanValidator + LuhnPanValidator),\n    IssuerData(300000..305999, CardIssuer.DinersClub, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(309500..309599, CardIssuer.DinersClub, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(360000..369999, CardIssuer.DinersClub, (14..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(380000..399999, CardIssuer.DinersClub, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(601100..601199, CardIssuer.Discover, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(622126..622925, CardIssuer.Discover, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(624000..626999, CardIssuer.Discover, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(628200..628899, CardIssuer.Discover, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(640000..659999, CardIssuer.Discover, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(352800..358999, CardIssuer.JCB, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(620000..629999, CardIssuer.UnionPay, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(810000..819999, CardIssuer.UnionPay, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(222100..272099, CardIssuer.MasterCard, (16..16).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(510000..559999, CardIssuer.MasterCard, (16..16).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(500000..509999, CardIssuer.MasterCard, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(560000..699999, CardIssuer.MasterCard, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(675900..675999, CardIssuer.MasterCard, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(676770..676770, CardIssuer.MasterCard, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(676774..676774, CardIssuer.MasterCard, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n    IssuerData(400000..499999, CardIssuer.Visa, (16..19).toList(), listOf(3), LengthPanValidator + LuhnPanValidator),\n)\n\n/**\n * This list is an extension of the list above and includes any custom card configurations as required by certain users.\n */\nprivate var CUSTOM_ISSUER_TABLE: MutableList<IssuerData> = mutableListOf()\n\n/**\n * Get an issuer from a complete or partial card number. If the pan is null, return an unknown issuer.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun getCardIssuer(pan: String?): CardIssuer = normalizeCardNumber(pan).let { normalizedPan ->\n    getIssuerData(normalizedPan)?.issuer ?: CardIssuer.Unknown\n}\n\n/**\n * Get the type of card from a complete or partial card number. If the pan is null, return an unknown type.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsuspend fun getCardType(context: Context, pan: String?): CardType = withContext(Dispatchers.IO) {\n    normalizeCardNumber(pan).let { normalizedPan ->\n        val iin = normalizedPan.iin().toIntOrNull() ?: -1\n        getTypeTable(context).filter { it.key.contains(iin) }.values.firstOrNull() ?: CardType.Unknown\n    }\n}\n\n/**\n * Determine if a PAN is valid.\n *\n * TODO: this should use a contract like the following once contracts are no longer experimental:\n * ```\n * contract { returns(true) implies (pan != null) }\n * ```\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun isValidPan(pan: String?): Boolean {\n    // contract { returns(true) implies (pan != null) }\n    return normalizeCardNumber(pan).let { normalizedPan ->\n        val iinData = getIssuerData(normalizedPan) ?: return false\n        iinData.panValidator.isValidPan(normalizedPan)\n    }\n}\n\n/**\n * Determine if an IIN is valid.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun isValidIin(iin: String?): Boolean {\n    // contract { returns(true) implies (iin != null) }\n    return normalizeCardNumber(iin).let { normalizedPan ->\n        getIssuerData(normalizedPan)?.issuer ?: CardIssuer.Unknown != CardIssuer.Unknown\n    }\n}\n\n/**\n * Determine if a CVC is valid based on an issuer.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun isValidCvc(cvc: String?, issuer: CardIssuer?): Boolean {\n    // contract { returns(true) implies (cvc != null) }\n    return normalizeCardNumber(cvc).let { cvcNumber ->\n        val issuerDataList = getIssuerData(issuer ?: CardIssuer.Unknown)\n        if (issuerDataList.isEmpty()) {\n            cvcNumber.length in VALID_CVC_LENGTHS\n        } else {\n            issuerDataList.any { cvcNumber.length in it.cvcLengths }\n        }\n    }\n}\n\n/**\n * Determine if the provided last four digits are valid.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun isValidPanLastFour(panLastFour: String?): Boolean {\n    // contract { returns(true) implies (panLastFour != null) }\n    return normalizeCardNumber(panLastFour).length == LAST_FOUR_LENGTH\n}\n\n/**\n * Get an IIN for a given PAN.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun iinFromPan(pan: String): String =\n    if (pan.length < IIN_LENGTH) {\n        pan.padEnd(IIN_LENGTH, '0')\n    } else {\n        pan.take(IIN_LENGTH)\n    }\n\n/**\n * Format a string as an IIN.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun String.iin(): String = iinFromPan(this)\n\n/**\n * Get the last four digits from a given PAN.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun lastFourFromPan(pan: String): String =\n    if (pan.length < LAST_FOUR_LENGTH) {\n        pan\n    } else {\n        pan.takeLast(LAST_FOUR_LENGTH)\n    }\n\n/**\n * Format a string as a payment card last four digits.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun String.lastFour(): String = lastFourFromPan(this)\n\n/**\n * Get data for a given IIN or PAN.\n */\n@CheckResult\ninternal fun getIssuerData(cardNumber: String): IssuerData? =\n    (CUSTOM_ISSUER_TABLE + ISSUER_TABLE).firstOrNull { iinFromPan(cardNumber).toInt() in it.iinRange }\n\n/**\n * Get data for a given [CardIssuer].\n */\n@CheckResult\nprivate fun getIssuerData(issuer: CardIssuer): List<IssuerData> =\n    (CUSTOM_ISSUER_TABLE + ISSUER_TABLE).filter { it.issuer == issuer }\n\n/**\n * Adds support for a new [CardIssuer]\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun supportCardIssuer(\n    iins: IntRange,\n    cardIssuer: CardIssuer,\n    panLengths: List<Int>,\n    cvcLengths: List<Int>,\n    validationFunction: PanValidator = LengthPanValidator + LuhnPanValidator,\n) = CUSTOM_ISSUER_TABLE.add(IssuerData(iins, cardIssuer, panLengths, cvcLengths, validationFunction))\n\n/**\n * Normalize a PAN by removing all non-numeric characters.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun normalizeCardNumber(cardNumber: String?) = cardNumber?.filter { it.isDigit() } ?: \"\"\n\n/**\n * Determine if the pan is valid or close to valid.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun isPossiblyValidPan(pan: String?): Boolean {\n    // contract { returns(true) implies (pan != null) }\n    return pan != null && pan.isDigitsOnly() && pan.length >= 7\n}\n\n/**\n * Determine if the pan is not close to being valid.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun isNotPossiblyValidPan(pan: String?): Boolean {\n    // contract { returns(false) implies (pan != null) }\n    return pan == null || !pan.isDigitsOnly() || pan.length < 10\n}\n\n/**\n * Determine if a card number (PAN, IIN, last four) possibly matches a required number (PAN, IIN, last four). This\n * method is designed to compare the same kinds of numbers. for example, a PAN compared to another PAN, or an IIN\n * compared to another IIN. This method will not correctly compare different values, such as an IIN to a PAN.\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun numberPossiblyMatches(scanned: String?, required: String?): Boolean =\n    scanned == required || (\n        scanned != null && scanned.isDigitsOnly() &&\n            (required == null || jaccardIndex(scanned, required) > JACCARD_SIMILARITY_THRESHOLD)\n        )\n\n/**\n * Determine if a given [pan] matches a required IIN and Last4\n */\n@CheckResult\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun panMatches(\n    requiredIin: String?,\n    requiredLastFour: String?,\n    pan: String\n): Boolean {\n    val matchesIin = requiredIin == null || requiredIin == pan.take(requiredIin.length)\n    val matchesLastFour = requiredLastFour == null || requiredLastFour == pan.takeLast(requiredLastFour.length)\n    return matchesIin && matchesLastFour\n}\n\n/**\n * Calculate the jaccard index (similarity) between two strings. Values can range from 0 (no\n * similarities) to 1 (the same). Note that this does not account for character order, so two\n * strings \"abcd\" and \"bdca\" have a jaccard index of 1.\n */\n@CheckResult\nprivate fun jaccardIndex(string1: String, string2: String): Double {\n    val set1 = string1.toSet()\n    val set2 = string2.toSet()\n\n    val intersection = set1.intersect(set2)\n\n    return intersection.size.toDouble() / (set1.size + set2.size - intersection.size)\n}\n\n/**\n * Determine if a string is digits only without using android libraries.\n */\nprivate fun String.isDigitsOnly() = this.toCharArray().all { it.isDigit() }\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/card/RequiresMatchingCard.kt",
    "content": "package com.getbouncer.scan.payment.card\n\n/**\n * An interface for a class that requires a card to match. This provides the methods used to determine if a given card\n * matches or does not match the required card.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ninterface RequiresMatchingCard {\n    val requiredIin: String?\n    val requiredLastFour: String?\n\n    /**\n     * Returns true if the card matches the [requiredIin] and/or [requiredLastFour], or if the two fields are null.\n     *\n     * TODO: use contracts when they are no longer experimental\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    fun matchesRequiredCard(pan: String?): Boolean {\n        // contract { returns(true) implies (pan != null) }\n        return pan != null && isValidPan(pan) && panMatches(requiredIin, requiredLastFour, pan)\n    }\n\n    /**\n     * Returns true if the required card fields are non-null and the given pan does not match.\n     *\n     * TODO: use contracts when they are no longer experimental\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    fun doesNotMatchRequiredCard(pan: String?): Boolean {\n        // contract { returns(true) implies (pan != null) }\n        return pan != null && isValidPan(pan) && !panMatches(requiredIin, requiredLastFour, pan)\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/ml/AlphabetDetect.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.util.Size\nimport com.getbouncer.scan.framework.FetchedData\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.UpdatingModelWebFetcher\nimport com.getbouncer.scan.framework.image.scale\nimport com.getbouncer.scan.framework.image.toMLImage\nimport com.getbouncer.scan.framework.ml.TFLAnalyzerFactory\nimport com.getbouncer.scan.framework.ml.TensorFlowLiteAnalyzer\nimport com.getbouncer.scan.framework.util.indexOfMax\nimport com.getbouncer.scan.payment.hasOpenGl31\nimport org.tensorflow.lite.Interpreter\nimport java.io.FileNotFoundException\nimport java.nio.ByteBuffer\n\nprivate val TRAINED_IMAGE_SIZE = Size(48, 48)\n\n/**\n * model returns whether or not there is a screen present\n */\nprivate const val NUM_CLASS = 27\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nclass AlphabetDetect private constructor(interpreter: Interpreter) :\n    TensorFlowLiteAnalyzer<\n        AlphabetDetect.Input,\n        ByteBuffer,\n        AlphabetDetect.Prediction,\n        Array<FloatArray>>(interpreter) {\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Input(val alphabetDetectImage: TrackedImage<Bitmap>)\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Prediction(val character: Char, val confidence: Float)\n\n    override suspend fun interpretMLOutput(data: Input, mlOutput: Array<FloatArray>): Prediction {\n        val prediction = mlOutput[0]\n        val index = prediction.indexOfMax()\n        val character = if (index != null && index > 0) {\n            // TODO: change this back once we support newer gradle versions\n//            ('A'.code - 1 + index).toChar()\n            ('A'.toInt() - 1 + index).toChar()\n        } else {\n            ' '\n        }\n\n        val confidence = index?.let { prediction[it] } ?: 0F\n\n        return Prediction(\n            character,\n            confidence\n        ).also {\n            data.alphabetDetectImage.tracker.trackResult(\"alphabet_detect_prediction_complete\")\n        }\n    }\n\n    override suspend fun transformData(data: Input): ByteBuffer = data.alphabetDetectImage.image\n        .scale(TRAINED_IMAGE_SIZE)\n        .toMLImage()\n        .getData()\n        .also {\n            data.alphabetDetectImage.tracker.trackResult(\"alphabet_detect_image_cropped\")\n        }\n\n    override suspend fun executeInference(\n        tfInterpreter: Interpreter,\n        data: ByteBuffer,\n    ): Array<FloatArray> {\n        val mlOutput = arrayOf(FloatArray(NUM_CLASS))\n        tfInterpreter.run(data, mlOutput)\n        return mlOutput\n    }\n\n    /**\n     * A factory for creating instances of this analyzer. This downloads the model from the web. If unable to download\n     * from the web, this will throw a [FileNotFoundException].\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    class Factory(\n        context: Context,\n        fetchedModel: FetchedData,\n        threads: Int = DEFAULT_THREADS\n    ) : TFLAnalyzerFactory<Input, Prediction, AlphabetDetect>(context, fetchedModel) {\n        companion object {\n            private const val USE_GPU = false\n            private const val DEFAULT_THREADS = 1\n        }\n\n        override val tfOptions: Interpreter.Options = Interpreter\n            .Options()\n            .setUseNNAPI(USE_GPU && hasOpenGl31(context))\n            .setNumThreads(threads)\n\n        override suspend fun newInstance(): AlphabetDetect? = createInterpreter()?.let { AlphabetDetect(it) }\n    }\n\n    /**\n     * A fetcher for downloading model data.\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    class ModelFetcher(context: Context) : UpdatingModelWebFetcher(context) {\n        override val defaultModelVersion: String = \"4.147.0.94.16\"\n        override val defaultModelHash: String = \"0693bf1962715e32f8d85ffefd8be9971d84ed554f25f4060aca2ca1f82c955b\"\n        override val defaultModelHashAlgorithm: String = \"SHA-256\"\n        override val defaultModelFileName: String = \"char_recognize.tflite\"\n        override val modelClass: String = \"char_recognize\"\n        override val modelFrameworkVersion: Int = 1\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/ml/AlphabetDetectModelManager.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport android.content.Context\nimport com.getbouncer.scan.framework.Fetcher\nimport com.getbouncer.scan.payment.ModelManager\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nobject AlphabetDetectModelManager : ModelManager() {\n    override fun getModelFetcher(context: Context): Fetcher = AlphabetDetect.ModelFetcher(context)\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/ml/CardDetect.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.Rect\nimport android.util.Size\nimport com.getbouncer.scan.framework.FetchedData\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.image.MLImage\nimport com.getbouncer.scan.framework.image.scale\nimport com.getbouncer.scan.framework.image.toMLImage\nimport com.getbouncer.scan.framework.ml.TFLAnalyzerFactory\nimport com.getbouncer.scan.framework.ml.TensorFlowLiteAnalyzer\nimport com.getbouncer.scan.framework.util.indexOfMax\nimport com.getbouncer.scan.payment.cropCameraPreviewToSquare\nimport com.getbouncer.scan.payment.hasOpenGl31\nimport kotlinx.coroutines.runBlocking\nimport org.tensorflow.lite.Interpreter\nimport java.nio.ByteBuffer\nimport kotlin.math.max\n\nprivate val TRAINED_IMAGE_SIZE = Size(224, 224)\n\n/** model returns whether or not there is a card present */\nprivate const val NUM_CLASS = 3\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nclass CardDetect private constructor(interpreter: Interpreter) :\n    TensorFlowLiteAnalyzer<CardDetect.Input, ByteBuffer, CardDetect.Prediction, Array<FloatArray>>(interpreter) {\n\n    companion object {\n        /**\n         * Convert a camera preview image into a CardDetect input\n         */\n        fun cameraPreviewToInput(\n            cameraPreviewImage: TrackedImage<Bitmap>,\n            previewBounds: Rect,\n            cardFinder: Rect,\n        ) = Input(\n            TrackedImage(\n                cropCameraPreviewToSquare(cameraPreviewImage.image, previewBounds, cardFinder)\n                    .scale(TRAINED_IMAGE_SIZE)\n                    .toMLImage()\n                    .also { runBlocking { cameraPreviewImage.tracker.trackResult(\"card_detect_image_cropped\") } },\n                cameraPreviewImage.tracker,\n            )\n        )\n    }\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Input(val cardDetectImage: TrackedImage<MLImage>)\n\n    /**\n     * A prediction returned by this analyzer.\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Prediction(\n        val side: Side,\n        val noCardProbability: Float,\n        val noPanProbability: Float,\n        val panProbability: Float,\n    ) {\n        val maxConfidence = max(max(noCardProbability, noPanProbability), panProbability)\n\n        /**\n         * Force a generic toString method to prevent leaking information about this class' parameters after R8. Without\n         * this method, this `data class` will automatically generate a toString which retains the original names of the\n         * parameters even after obfuscation.\n         */\n        override fun toString(): String {\n            return \"Prediction\"\n        }\n\n        enum class Side {\n            NO_CARD,\n            NO_PAN,\n            PAN,\n        }\n    }\n\n    override suspend fun interpretMLOutput(data: Input, mlOutput: Array<FloatArray>): Prediction {\n        val side = when (val index = mlOutput[0].indexOfMax()) {\n            0 -> Prediction.Side.NO_PAN\n            1 -> Prediction.Side.NO_CARD\n            2 -> Prediction.Side.PAN\n            else -> throw EnumConstantNotPresentException(\n                Prediction.Side::class.java,\n                index.toString(),\n            )\n        }\n\n        data.cardDetectImage.tracker.trackResult(\"card_detect_prediction_complete\")\n\n        return Prediction(\n            side = side,\n            noPanProbability = mlOutput[0][0],\n            noCardProbability = mlOutput[0][1],\n            panProbability = mlOutput[0][2],\n        )\n    }\n\n    override suspend fun transformData(data: Input): ByteBuffer = data.cardDetectImage.image.getData()\n\n    override suspend fun executeInference(\n        tfInterpreter: Interpreter,\n        data: ByteBuffer,\n    ): Array<FloatArray> {\n        val mlOutput = arrayOf(FloatArray(NUM_CLASS))\n        tfInterpreter.run(data, mlOutput)\n        return mlOutput\n    }\n\n    /**\n     * A factory for creating instances of this analyzer.\n     */\n    class Factory(\n        context: Context,\n        fetchedModel: FetchedData,\n        threads: Int = DEFAULT_THREADS,\n    ) : TFLAnalyzerFactory<Input, Prediction, CardDetect>(context, fetchedModel) {\n        companion object {\n            private const val USE_GPU = false\n            private const val DEFAULT_THREADS = 4\n        }\n\n        override val tfOptions: Interpreter.Options = Interpreter\n            .Options()\n            .setUseNNAPI(USE_GPU && hasOpenGl31(context))\n            .setNumThreads(threads)\n\n        override suspend fun newInstance(): CardDetect? = createInterpreter()?.let { CardDetect(it) }\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/ml/CardDetectModelManager.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport android.content.Context\nimport android.util.Log\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.Fetcher\nimport com.getbouncer.scan.framework.UpdatingModelWebFetcher\nimport com.getbouncer.scan.framework.UpdatingResourceFetcher\nimport com.getbouncer.scan.framework.assetFileExists\nimport com.getbouncer.scan.payment.ModelManager\n\nprivate const val CARD_DETECT_ASSET_FULL = \"ux_0_5_23_16.tflite\"\nprivate const val CARD_DETECT_ASSET_MINIMAL = \"UX.0.25.106.8.tflite\"\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nobject CardDetectModelManager : ModelManager() {\n    override fun getModelFetcher(context: Context): Fetcher = when {\n        assetFileExists(context, CARD_DETECT_ASSET_FULL) -> {\n            Log.d(Config.logTag, \"Full card detect available in assets\")\n            object : UpdatingResourceFetcher(context) {\n                override val assetFileName: String = CARD_DETECT_ASSET_FULL\n                override val resourceModelVersion: String = \"0.5.23.16\"\n                override val resourceModelHash: String = \"ea51ca5c693a4b8733b1cf1a63557a713a13fabf0bcb724385077694e63a51a7\"\n                override val resourceModelHashAlgorithm: String = \"SHA-256\"\n                override val modelClass: String = \"card_detection\"\n                override val modelFrameworkVersion: Int = 1\n            }\n        }\n        assetFileExists(context, CARD_DETECT_ASSET_MINIMAL) -> {\n            Log.d(Config.logTag, \"Minimal card detect available in assets\")\n            object : UpdatingResourceFetcher(context) {\n                override val assetFileName: String = CARD_DETECT_ASSET_MINIMAL\n                override val resourceModelVersion: String = \"0.25.106.8\"\n                override val resourceModelHash: String = \"c2a39c9034a9f0073933488021676c46910cec0d1bf330ac22a908dcd7dd448a\"\n                override val resourceModelHashAlgorithm: String = \"SHA-256\"\n                override val modelClass: String = \"card_detection\"\n                override val modelFrameworkVersion: Int = 1\n            }\n        }\n        else -> {\n            Log.d(Config.logTag, \"No card detect available in assets\")\n            object : UpdatingModelWebFetcher(context) {\n                override val defaultModelFileName: String = \"UX.0.5.23.16.tflite\"\n                override val defaultModelVersion: String = \"0.5.23.16\"\n                override val defaultModelHash: String = \"ea51ca5c693a4b8733b1cf1a63557a713a13fabf0bcb724385077694e63a51a7\"\n                override val defaultModelHashAlgorithm: String = \"SHA-256\"\n                override val modelClass: String = \"card_detection\"\n                override val modelFrameworkVersion: Int = 1\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/ml/ExpiryDetect.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.Rect\nimport android.graphics.RectF\nimport android.util.Size\nimport com.getbouncer.scan.framework.FetchedData\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.UpdatingModelWebFetcher\nimport com.getbouncer.scan.framework.image.crop\nimport com.getbouncer.scan.framework.image.scale\nimport com.getbouncer.scan.framework.image.size\nimport com.getbouncer.scan.framework.image.toMLImage\nimport com.getbouncer.scan.framework.ml.TFLAnalyzerFactory\nimport com.getbouncer.scan.framework.ml.TensorFlowLiteAnalyzer\nimport com.getbouncer.scan.framework.ml.greedyNonMaxSuppression\nimport com.getbouncer.scan.framework.util.indexOfMax\nimport com.getbouncer.scan.framework.util.scaled\nimport com.getbouncer.scan.payment.card.formatExpiry\nimport com.getbouncer.scan.payment.card.isValidExpiry\nimport com.getbouncer.scan.payment.card.isValidMonth\nimport com.getbouncer.scan.payment.cropCameraPreviewToSquare\nimport com.getbouncer.scan.payment.hasOpenGl31\nimport kotlinx.coroutines.runBlocking\nimport org.tensorflow.lite.Interpreter\nimport java.io.FileNotFoundException\nimport java.nio.ByteBuffer\nimport kotlin.math.roundToInt\n\nprivate val TRAINED_IMAGE_SIZE = Size(80, 36)\n\nprivate const val NUM_CLASS = 11\nprivate const val NUM_PREDICTIONS = 17\nprivate const val BACKGROUND_CLASS = 10\n\nprivate val ASPECT_RATIO = TRAINED_IMAGE_SIZE.height.toFloat() / TRAINED_IMAGE_SIZE.width.toFloat()\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nclass ExpiryDetect private constructor(interpreter: Interpreter) :\n    TensorFlowLiteAnalyzer<\n        ExpiryDetect.Input,\n        ByteBuffer,\n        ExpiryDetect.Prediction,\n        Array<Array<Array<FloatArray>>>>(interpreter) {\n\n    companion object {\n        /**\n         * Convert a camera preview image into a CardDetect input\n         */\n        fun cameraPreviewToInput(\n            cameraPreviewImage: TrackedImage<Bitmap>,\n            previewBounds: Rect,\n            viewFinder: Rect,\n            expiryBox: RectF,\n        ) = Input(\n            TrackedImage(\n                cropCameraPreviewToSquare(\n                    cameraPreviewImage = cameraPreviewImage.image,\n                    previewBounds = previewBounds,\n                    viewFinder = viewFinder,\n                )\n                    .also { runBlocking { cameraPreviewImage.tracker.trackResult(\"expiry_detect_image_cropped\") } },\n                cameraPreviewImage.tracker,\n            ),\n            expiryBox\n        )\n    }\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Input(val expiryDetectImage: TrackedImage<Bitmap>, val expiryBox: RectF)\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Prediction(val expiry: Expiry?)\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Expiry(val month: String, val year: String) : Comparable<Expiry> {\n        override fun toString() = formatExpiry(\n            day = null,\n            month = month,\n            year = year\n        )\n\n        override fun compareTo(other: Expiry): Int =\n            (year.toIntOrNull() ?: 0) * 100 + (month.toIntOrNull() ?: 0)\n                .compareTo((other.year.toIntOrNull() ?: 0) * 100 + (other.month.toIntOrNull() ?: 0))\n\n        fun isValidExpiry() = isValidExpiry(null, month, year)\n    }\n\n    private data class Digit(val digit: Int, val confidence: Float)\n\n    override suspend fun interpretMLOutput(data: Input, mlOutput: Array<Array<Array<FloatArray>>>): Prediction {\n        val output = mlOutput[0][0].mapNotNull {\n            it.indexOfMax()?.let { maxIndex ->\n                Digit(maxIndex, it[maxIndex])\n            }\n        }\n\n        val (newDigits, confidence) = output.map {\n            Pair(it.digit, it.confidence)\n        }.unzip()\n\n        val digits = greedyNonMaxSuppression(\n            newDigits.toTypedArray(),\n            confidence.toFloatArray(),\n            BACKGROUND_CLASS\n        ).filter { it != BACKGROUND_CLASS }\n\n        return if (digits.size == 4 || (digits.size == 5 && digits[2] == 1)) {\n            // process if we get exactly 4 digits, OR it's five digits and the middle prediction\n            // is a 1 - this is because we sometimes mistake '/' for '1'\n            val month = \"${digits[0]}${digits[1]}\"\n            val year = \"20${digits[digits.size - 2]}${digits[digits.size - 1]}\"\n            if (isValidMonth(month)) {\n                Prediction(Expiry(month, year))\n            } else {\n                Prediction(null)\n            }\n        } else {\n            Prediction(null)\n        }.also {\n            data.expiryDetectImage.tracker.trackResult(\"expiry_detect_prediction_complete\")\n        }\n    }\n\n    override suspend fun transformData(data: Input): ByteBuffer {\n        val targetAspectRatio = ASPECT_RATIO\n        val scaledRect = data.expiryBox.scaled(data.expiryDetectImage.image.size())\n        val scaledExpRectNewHeight = scaledRect.width() * targetAspectRatio\n\n        val rect = Rect(\n            scaledRect.left.roundToInt(),\n            (scaledRect.centerY() - scaledExpRectNewHeight / 2).roundToInt(),\n            scaledRect.right.roundToInt(),\n            (scaledRect.centerY() + scaledExpRectNewHeight / 2).roundToInt()\n        )\n\n        return data.expiryDetectImage.image\n            .crop(rect)\n            .scale(TRAINED_IMAGE_SIZE)\n            .toMLImage()\n            .getData()\n            .also {\n                data.expiryDetectImage.tracker.trackResult(\"expiry_detect_image_cropped\")\n            }\n    }\n\n    override suspend fun executeInference(\n        tfInterpreter: Interpreter,\n        data: ByteBuffer,\n    ): Array<Array<Array<FloatArray>>> {\n        val mlOutput = arrayOf(arrayOf(Array(NUM_PREDICTIONS) { FloatArray(NUM_CLASS) }))\n        tfInterpreter.run(data, mlOutput)\n        return mlOutput\n    }\n\n    /**\n     * A factory for creating instances of this analyzer. This downloads the model from the web. If unable to download\n     * from the web, this will throw a [FileNotFoundException].\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    class Factory(\n        context: Context,\n        fetchedModel: FetchedData,\n        threads: Int = DEFAULT_THREADS\n    ) : TFLAnalyzerFactory<Input, Prediction, ExpiryDetect>(context, fetchedModel) {\n        companion object {\n            private const val USE_GPU = false\n            private const val DEFAULT_THREADS = 1\n        }\n\n        override val tfOptions: Interpreter.Options = Interpreter\n            .Options()\n            .setUseNNAPI(USE_GPU && hasOpenGl31(context))\n            .setNumThreads(threads)\n\n        override suspend fun newInstance(): ExpiryDetect? = createInterpreter()?.let { ExpiryDetect(it) }\n    }\n\n    /**\n     * A fetcher for downloading model data.\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    class ModelFetcher(context: Context) : UpdatingModelWebFetcher(context) {\n        override val defaultModelVersion: String = \"0.0.1.16\"\n        override val defaultModelHash: String = \"55eea0d57239a7e92904fb15209963f7236bd06919275bdeb0a765a94b559c97\"\n        override val defaultModelHashAlgorithm: String = \"SHA-256\"\n        override val defaultModelFileName: String = \"fourrecognize.tflite\"\n        override val modelClass: String = \"four_recognize\"\n        override val modelFrameworkVersion: Int = 1\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/ml/ExpiryDetectModelManager.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport android.content.Context\nimport com.getbouncer.scan.framework.Fetcher\nimport com.getbouncer.scan.payment.ModelManager\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nobject ExpiryDetectModelManager : ModelManager() {\n    override fun getModelFetcher(context: Context): Fetcher = ExpiryDetect.ModelFetcher(context)\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/ml/SSDOcr.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.Rect\nimport android.util.Size\nimport androidx.annotation.VisibleForTesting\nimport com.getbouncer.scan.framework.FetchedData\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.image.MLImage\nimport com.getbouncer.scan.framework.image.scale\nimport com.getbouncer.scan.framework.image.toMLImage\nimport com.getbouncer.scan.framework.ml.TFLAnalyzerFactory\nimport com.getbouncer.scan.framework.ml.TensorFlowLiteAnalyzer\nimport com.getbouncer.scan.framework.ml.ssd.adjustLocations\nimport com.getbouncer.scan.framework.ml.ssd.softMax\nimport com.getbouncer.scan.framework.ml.ssd.toRectForm\nimport com.getbouncer.scan.framework.util.reshape\nimport com.getbouncer.scan.payment.cropCameraPreviewToViewFinder\nimport com.getbouncer.scan.payment.hasOpenGl31\nimport com.getbouncer.scan.payment.ml.ssd.DetectionBox\nimport com.getbouncer.scan.payment.ml.ssd.OcrFeatureMapSizes\nimport com.getbouncer.scan.payment.ml.ssd.combinePriors\nimport com.getbouncer.scan.payment.ml.ssd.determineLayoutAndFilter\nimport com.getbouncer.scan.payment.ml.ssd.extractPredictions\nimport com.getbouncer.scan.payment.ml.ssd.rearrangeOCRArray\nimport kotlinx.coroutines.runBlocking\nimport org.tensorflow.lite.Interpreter\nimport java.nio.ByteBuffer\n\n/** Training images are normalized with mean 127.5 and std 128.5. */\nprivate const val IMAGE_MEAN = 127.5f\nprivate const val IMAGE_STD = 128.5f\n\n/**\n * We use the output from last two layers with feature maps 19x19 and 10x10\n * and for each feature map activation we have 6 priors, so total priors are\n * 19x19x6 + 10x10x6 = 2766\n */\nprivate const val NUM_OF_PRIORS = 3420\n\n/**\n * For each activation in our feature map, we have predictions for 6 bounding boxes\n * of different aspect ratios\n */\nprivate const val NUM_OF_PRIORS_PER_ACTIVATION = 3\n\n/**\n * We can detect a total of 10 numbers (0 - 9) plus the background class\n */\nprivate const val NUM_OF_CLASSES = 11\n\n/**\n * Each prior or bounding box can be represented by 4 coordinates\n * XMin, YMin, XMax, YMax.\n */\nprivate const val NUM_OF_COORDINATES = 4\n\n/**\n * Represents the total number of data points for locations\n */\nprivate const val NUM_LOC = NUM_OF_COORDINATES * NUM_OF_PRIORS\n\n/**\n * Represents the total number of data points for classes\n */\nprivate const val NUM_CLASS = NUM_OF_CLASSES * NUM_OF_PRIORS\n\nprivate const val PROB_THRESHOLD = 0.50f\nprivate const val IOU_THRESHOLD = 0.50f\nprivate const val CENTER_VARIANCE = 0.1f\nprivate const val SIZE_VARIANCE = 0.2f\nprivate const val LIMIT = 20\n\n@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)\ninternal const val VERTICAL_THRESHOLD = 2.0f\n\nprivate val FEATURE_MAP_SIZES =\n    OcrFeatureMapSizes(\n        layerOneWidth = 38,\n        layerOneHeight = 24,\n        layerTwoWidth = 19,\n        layerTwoHeight = 12\n    )\n\n/**\n * This value should never change, and is thread safe.\n */\nprivate val PRIORS = combinePriors(SSDOcr.Factory.TRAINED_IMAGE_SIZE)\n\n/**\n * This model performs SSD OCR recognition on a card.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nclass SSDOcr private constructor(interpreter: Interpreter) :\n    TensorFlowLiteAnalyzer<SSDOcr.Input, Array<ByteBuffer>, SSDOcr.Prediction, Map<Int, Array<FloatArray>>>(\n        interpreter\n    ) {\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Input(val ssdOcrImage: TrackedImage<MLImage>)\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Prediction(val pan: String, val detectedBoxes: List<DetectionBox>)\n\n    companion object {\n        /**\n         * Convert a camera preview image into a SSDOcr input\n         */\n        fun cameraPreviewToInput(\n            cameraPreviewImage: TrackedImage<Bitmap>,\n            previewBounds: Rect,\n            cardFinder: Rect\n        ) = Input(\n            TrackedImage(\n                cropCameraPreviewToViewFinder(cameraPreviewImage.image, previewBounds, cardFinder)\n                    .scale(Factory.TRAINED_IMAGE_SIZE)\n                    .toMLImage(mean = IMAGE_MEAN, std = IMAGE_STD).also {\n                        runBlocking { cameraPreviewImage.tracker.trackResult(\"ocr_image_transform\") }\n                    },\n                cameraPreviewImage.tracker\n            )\n        )\n    }\n\n    override suspend fun transformData(data: Input): Array<ByteBuffer> = arrayOf(data.ssdOcrImage.image.getData())\n\n    override suspend fun interpretMLOutput(\n        data: Input,\n        mlOutput: Map<Int, Array<FloatArray>>,\n    ): Prediction {\n        val outputClasses = mlOutput[0] ?: arrayOf(FloatArray(NUM_CLASS))\n        val outputLocations = mlOutput[1] ?: arrayOf(FloatArray(NUM_LOC))\n\n        val boxes = rearrangeOCRArray(\n            locations = outputLocations,\n            featureMapSizes = FEATURE_MAP_SIZES,\n            numberOfPriors = NUM_OF_PRIORS_PER_ACTIVATION,\n            locationsPerPrior = NUM_OF_COORDINATES,\n        ).reshape(NUM_OF_COORDINATES)\n        boxes.adjustLocations(\n            priors = PRIORS,\n            centerVariance = CENTER_VARIANCE,\n            sizeVariance = SIZE_VARIANCE,\n        )\n        boxes.forEach { it.toRectForm() }\n\n        val scores = rearrangeOCRArray(\n            locations = outputClasses,\n            featureMapSizes = FEATURE_MAP_SIZES,\n            numberOfPriors = NUM_OF_PRIORS_PER_ACTIVATION,\n            locationsPerPrior = NUM_OF_CLASSES,\n        ).reshape(NUM_OF_CLASSES)\n        scores.forEach { it.softMax() }\n\n        val detectedBoxes = determineLayoutAndFilter(\n            extractPredictions(\n                scores = scores,\n                boxes = boxes,\n                probabilityThreshold = PROB_THRESHOLD,\n                intersectionOverUnionThreshold = IOU_THRESHOLD,\n                limit = LIMIT,\n                classifierToLabel = { if (it == 10) 0 else it },\n            ),\n            VERTICAL_THRESHOLD,\n        )\n\n        val predictedNumber = detectedBoxes.map { it.label }.joinToString(\"\")\n\n        data.ssdOcrImage.tracker.trackResult(\"ocr_prediction_complete\")\n        return Prediction(predictedNumber, detectedBoxes)\n    }\n\n    override suspend fun executeInference(\n        tfInterpreter: Interpreter,\n        data: Array<ByteBuffer>,\n    ): Map<Int, Array<FloatArray>> {\n        val mlOutput = mapOf(\n            0 to arrayOf(FloatArray(NUM_CLASS)),\n            1 to arrayOf(FloatArray(NUM_LOC)),\n        )\n\n        tfInterpreter.runForMultipleInputsOutputs(data, mlOutput)\n        return mlOutput\n    }\n\n    /**\n     * A factory for creating instances of this analyzer.\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    class Factory(\n        context: Context,\n        fetchedModel: FetchedData,\n        threads: Int = DEFAULT_THREADS,\n    ) : TFLAnalyzerFactory<Input, Prediction, SSDOcr>(context, fetchedModel) {\n        companion object {\n            private const val USE_GPU = false\n            private const val DEFAULT_THREADS = 4\n\n            val TRAINED_IMAGE_SIZE = Size(600, 375)\n        }\n\n        override val tfOptions: Interpreter.Options = Interpreter\n            .Options()\n            .setUseNNAPI(USE_GPU && hasOpenGl31(context.applicationContext))\n            .setNumThreads(threads)\n\n        override suspend fun newInstance(): SSDOcr? = createInterpreter()?.let { SSDOcr(it) }\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/ml/SSDOcrModelManager.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport android.content.Context\nimport android.util.Log\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.Fetcher\nimport com.getbouncer.scan.framework.UpdatingModelWebFetcher\nimport com.getbouncer.scan.framework.UpdatingResourceFetcher\nimport com.getbouncer.scan.framework.assetFileExists\nimport com.getbouncer.scan.payment.ModelManager\n\nprivate const val OCR_ASSET_FULL = \"darknite_1_1_1_16.tflite\"\nprivate const val OCR_ASSET_MINIMAL = \"mb2_brex_metal_synthetic_svhnextra_epoch_3_5_98_8.tflite\"\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nobject SSDOcrModelManager : ModelManager() {\n    override fun getModelFetcher(context: Context): Fetcher = when {\n        assetFileExists(context, OCR_ASSET_FULL) -> {\n            Log.d(Config.logTag, \"Full ocr available in assets\")\n            object : UpdatingResourceFetcher(context) {\n                override val assetFileName: String = OCR_ASSET_FULL\n                override val resourceModelVersion: String = \"1.1.1.16\"\n                override val resourceModelHash: String = \"8d8e3f79aa0783ab0cfa5c8d65d663a9da6ba99401efb2298aaaee387c3b00d6\"\n                override val resourceModelHashAlgorithm: String = \"SHA-256\"\n                override val modelClass: String = \"ocr\"\n                override val modelFrameworkVersion: Int = 1\n            }\n        }\n        assetFileExists(context, OCR_ASSET_MINIMAL) -> {\n            Log.d(Config.logTag, \"Minimal ocr available in assets\")\n            object : UpdatingResourceFetcher(context) {\n                override val assetFileName: String = OCR_ASSET_MINIMAL\n                override val resourceModelVersion: String = \"3.5.98.8\"\n                override val resourceModelHash: String = \"a4739fa49caa3ff88e7ff1145c9334ee4cbf64354e91131d02d98d7bfd4c35cf\"\n                override val resourceModelHashAlgorithm: String = \"SHA-256\"\n                override val modelClass: String = \"ocr\"\n                override val modelFrameworkVersion: Int = 1\n            }\n        }\n        else -> {\n            Log.d(Config.logTag, \"No ocr available in assets\")\n            object : UpdatingModelWebFetcher(context) {\n                override val defaultModelFileName: String = \"darknite_1_1_1_16.tflite\"\n                override val defaultModelVersion: String = \"1.1.1.16\"\n                override val defaultModelHash: String = \"8d8e3f79aa0783ab0cfa5c8d65d663a9da6ba99401efb2298aaaee387c3b00d6\"\n                override val defaultModelHashAlgorithm: String = \"SHA-256\"\n                override val modelClass: String = \"ocr\"\n                override val modelFrameworkVersion: Int = 1\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/ml/TextDetect.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.Rect\nimport android.graphics.RectF\nimport android.util.Log\nimport android.util.Size\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.FetchedData\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.UpdatingModelWebFetcher\nimport com.getbouncer.scan.framework.image.MLImage\nimport com.getbouncer.scan.framework.image.scale\nimport com.getbouncer.scan.framework.image.toMLImage\nimport com.getbouncer.scan.framework.ml.TFLAnalyzerFactory\nimport com.getbouncer.scan.framework.ml.TensorFlowLiteAnalyzer\nimport com.getbouncer.scan.framework.ml.hardNonMaximumSuppression\nimport com.getbouncer.scan.framework.ml.ssd.rectForm\nimport com.getbouncer.scan.payment.cropCameraPreviewToSquare\nimport com.getbouncer.scan.payment.hasOpenGl31\nimport com.getbouncer.scan.payment.ml.ssd.DetectionBox\nimport com.getbouncer.scan.payment.ml.yolo.processYoloLayer\nimport kotlinx.coroutines.runBlocking\nimport org.tensorflow.lite.Interpreter\nimport java.io.FileNotFoundException\nimport java.nio.ByteBuffer\nimport kotlin.math.abs\nimport kotlin.math.floor\nimport kotlin.math.ln\nimport kotlin.math.max\nimport kotlin.math.pow\n\nprivate val TRAINED_IMAGE_SIZE = Size(416, 416)\n\nprivate const val YOLO_POST_PROCESS_CONFIDENCE_THRESHOLD = 0.5f\nprivate val YOLO_ANCHORS = arrayOf(\n    arrayOf(\n        Pair(81, 82),\n        Pair(135, 169),\n        Pair(344, 319)\n    ),\n    arrayOf(\n        Pair(10, 14),\n        Pair(23, 27),\n        Pair(37, 58)\n    )\n)\n\n/**\n * Model returns the following 4 classes:\n * 0. MM/YY Expiration Date\n * 1: Mostly letters - At most 1 non-character and more letters than other characters\n * 2: Mostly numbers - At most 2 non-numbers and more numbers than other characters\n * 3: Mixed - mix of numbers and characters (any remaining cases)\n **/\nprivate enum class LABELS {\n    EXPIRATION_DATE,\n    LETTERS,\n    NUMBERS,\n    MIXED\n}\nprivate val NUM_CLASS = LABELS.values().size\n\nprivate val LAYER_1_SIZE = Size(13, 13)\nprivate val LAYER_2_SIZE = Size(26, 26)\n\n// A YOLO3 constant derived from NUM_CLASS\nprivate val DIM_Z = (NUM_CLASS + 5) * 3\n\nprivate const val BOX_TOP_DELTA_THRESHOLD = 0.4F\nprivate const val HEIGHT_RATIO_THRESHOLD = 0.3F\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nclass TextDetect private constructor(interpreter: Interpreter) :\n    TensorFlowLiteAnalyzer<\n        TextDetect.Input,\n        Array<ByteBuffer>,\n        TextDetect.Prediction,\n        Map<Int, Array<Array<Array<FloatArray>>>>>(interpreter) {\n\n    companion object {\n        /**\n         * Convert a camera preview image into a CardDetect input\n         */\n        fun cameraPreviewToInput(\n            cameraPreviewImage: TrackedImage<Bitmap>,\n            previewBounds: Rect,\n            viewFinder: Rect,\n        ) = Input(\n            TrackedImage(\n                cropCameraPreviewToSquare(\n                    cameraPreviewImage = cameraPreviewImage.image,\n                    previewBounds = previewBounds,\n                    viewFinder = viewFinder,\n                )\n                    .scale(TRAINED_IMAGE_SIZE)\n                    .toMLImage()\n                    .also { runBlocking { cameraPreviewImage.tracker.trackResult(\"text_detect_image_cropped\") } },\n                cameraPreviewImage.tracker,\n            )\n        )\n    }\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Input(\n        val textDetectImage: TrackedImage<MLImage>,\n    )\n\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    data class Prediction(\n        val allObjects: List<DetectionBox>,\n        val nameBoxes: List<DetectionBox>,\n        val expiryBoxes: List<DetectionBox>\n    )\n\n    private data class MergedBox(val box: DetectionBox, val subBoxes: List<DetectionBox>)\n\n    private fun postProcessYolo(rawMlOutput: Map<Int, Array<Array<Array<FloatArray>>>>): List<DetectionBox> {\n        val results = mutableListOf<DetectionBox>()\n\n        val layerZero = rawMlOutput[0]\n        if (layerZero?.isNotEmpty() == true) {\n            results.addAll(\n                processYoloLayer(\n                    layerZero.first(),\n                    YOLO_ANCHORS[0],\n                    TRAINED_IMAGE_SIZE,\n                    NUM_CLASS,\n                    YOLO_POST_PROCESS_CONFIDENCE_THRESHOLD\n                )\n            )\n        } else {\n            Log.w(Config.logTag, \"Unable to resolve YOLO layer 0\")\n        }\n\n        val layerOne = rawMlOutput[1]\n        if (layerOne?.isNotEmpty() == true) {\n            results.addAll(\n                processYoloLayer(\n                    layerOne.first(),\n                    YOLO_ANCHORS[1],\n                    TRAINED_IMAGE_SIZE,\n                    NUM_CLASS,\n                    YOLO_POST_PROCESS_CONFIDENCE_THRESHOLD\n                )\n            )\n        } else {\n            Log.w(Config.logTag, \"Unable to resolve YOLO layer 1\")\n        }\n\n        return results\n    }\n\n    override suspend fun interpretMLOutput(\n        data: Input,\n        mlOutput: Map<Int, Array<Array<Array<FloatArray>>>>\n    ): Prediction {\n        val outputBoxes = extractPredictions(postProcessYolo(mlOutput))\n        val (panBoxes, nameBoxes) = getNameBox(outputBoxes) ?: (null to null)\n\n        // add our merged pan and name boxes into the set of objects we return\n        // for debugging purposes\n        val allObjects = mutableListOf<DetectionBox>()\n        allObjects.addAll(outputBoxes)\n        if (nameBoxes != null && panBoxes != null) {\n            allObjects.add(\n                DetectionBox(\n                    nameBoxes.box.rect,\n                    nameBoxes.box.confidence,\n                    nameBoxes.box.label + NUM_CLASS // making up a new label for debugging purposes\n                )\n            )\n            allObjects.add(\n                DetectionBox(\n                    panBoxes.box.rect,\n                    panBoxes.box.confidence,\n                    panBoxes.box.label + NUM_CLASS // making up a new label for debugging purposes\n                )\n            )\n        }\n\n        return Prediction(\n            allObjects,\n            nameBoxes?.subBoxes ?: emptyList(),\n            outputBoxes.filter { it.label == LABELS.EXPIRATION_DATE.ordinal }.sortedByDescending { it.confidence }.take(2)\n        ).also {\n            data.textDetectImage.tracker.trackResult(\"text_detect_prediction_complete\")\n        }\n    }\n\n    /**\n     * Find all boxes that are \"close\" to the [originalBox], including itself. The returned list of boxes is sorted\n     * from leftmost to rightmost (by the x value)\n     *\n     * A box is defined as close to the [originalBox] if:\n     * 1. it is the [originalBox]\n     * 2. it's not an Expiry box (Expiry boxes should never be merged)\n     * 3. the top of the boxes are mostly aligned\n     * 4. The height of the box is similar to the height of the original box\n     */\n    private fun getCloseBoxes(originalBox: DetectionBox, boxes: List<DetectionBox>): List<DetectionBox> {\n        fun isSimilarYStart(it: DetectionBox) = abs(it.rect.top - originalBox.rect.top) <\n            originalBox.rect.height() * BOX_TOP_DELTA_THRESHOLD\n        fun isSimilarHeight(it: DetectionBox) = abs(1F - it.rect.height() / originalBox.rect.height()) < HEIGHT_RATIO_THRESHOLD\n        return boxes.filter {\n            (it == originalBox) || (it.label != LABELS.EXPIRATION_DATE.ordinal && isSimilarHeight(it)) && isSimilarYStart(it)\n        }.sortedBy { it.rect.left }\n    }\n\n    /**\n     * Run NMS on detection results\n     */\n    private fun extractPredictions(raw: List<DetectionBox>): List<DetectionBox> {\n        val predictions = mutableListOf<DetectionBox>()\n\n        val scores = raw.map { it.confidence }\n        val boxes = raw.map { rectForm(it.rect.left, it.rect.top, it.rect.right, it.rect.bottom) }\n\n        val indexes =\n            hardNonMaximumSuppression(\n                boxes = boxes.toTypedArray(),\n                probabilities = scores.toFloatArray(),\n                iouThreshold = 0.4f,\n                limit = null\n            )\n\n        for (index in indexes) {\n            predictions.add(\n                raw[index]\n            )\n        }\n        return predictions\n    }\n\n    /**\n     * From a list of raw boxes, determine the likely PAN location and the name\n     */\n    private fun getNameBox(boxes: List<DetectionBox>): Pair<MergedBox, MergedBox?>? {\n        val metaBoxes = mergeAllBoxes(boxes)\n        val panMetaBox = getPanBox(metaBoxes) ?: return null\n        return Pair(\n            panMetaBox,\n            metaBoxes.asSequence().filter {\n                // filter for merged boxes that are classified as text\n                it.box.label == LABELS.LETTERS.ordinal\n            }.map {\n                // gather the name score for this box\n                Pair(predictionToNameScoreNew(it, panMetaBox), it)\n            }.maxByOrNull {\n                // get the box for the highest score\n                it.first\n            }?.second\n        )\n    }\n\n    /**\n     * Given a candidate box, returns the name score derived from its relationship with the panBox\n     */\n    private fun predictionToNameScoreNew(candidateBox: MergedBox, panBox: MergedBox): Float {\n        val partialScores = arrayOf(\n            ln(candidateBox.box.confidence),\n            getXDistScore(candidateBox.box, panBox.box),\n            getHeightScore(candidateBox.box, panBox.box),\n            calculateNameWidthScore(candidateBox)\n        )\n        return partialScores.sum()\n    }\n\n    /**\n     * Find the MergedBox who's the most like to be the PAN.\n     * Returns the mergedbox that has the most number of merged digit boxes\n     */\n    private fun getPanBox(boxes: List<MergedBox>) = boxes.maxByOrNull { mergedBox ->\n        mergedBox.subBoxes.filter {\n            it.label == 2\n        }.size\n    }\n\n    /**\n     * Given a list of [DetectionBox], [boxes], returns a list of [MergedBox] where\n     * each MergedBox consists of a sublist of [boxes] who are close enough in size and orientation.\n     * The goal is to join words belonging to the same entity to the same [MergedBox]. For example,\n     * we want to join the four separate detection things\n     */\n    private fun mergeAllBoxes(boxes: List<DetectionBox>): List<MergedBox> {\n        val unmergedBoxes = boxes.toMutableList()\n        val mergedBoxes = mutableListOf<MergedBox>()\n\n        while (unmergedBoxes.isNotEmpty()) {\n            val candidateBox = unmergedBoxes.first()\n            val subBoxes = getCloseBoxes(candidateBox, unmergedBoxes)\n            val metaConfidence = subBoxes.maxByOrNull { it.confidence }?.confidence ?: 0f\n            mergedBoxes.add(\n                MergedBox(\n                    DetectionBox(\n                        RectF(\n                            subBoxes.first().rect.left,\n                            subBoxes.first().rect.top,\n                            subBoxes.last().rect.right,\n                            subBoxes.last().rect.bottom\n                        ),\n                        metaConfidence,\n                        subBoxes.first().label\n                    ),\n                    subBoxes\n                )\n            )\n            unmergedBoxes.removeAll(subBoxes)\n        }\n        return mergedBoxes\n    }\n\n    /**\n     * Calculates a component of the name score derived from the delta of rect.left the\n     * top left corner of the proposed name box and the pan box\n     */\n    private fun getXDistScore(prediction: DetectionBox, panBox: DetectionBox): Float {\n        val mean = -0.015011064206789725f\n        val std = 0.45382757813736924f\n        val panHeight = panBox.rect.height()\n        val xDist = (panBox.rect.left - prediction.rect.left) / panHeight\n        return (-1 / 2f) * ((xDist - mean) / std).pow(2)\n    }\n\n    /**\n     * Calculates a component of the name score derived from the height ratio between the\n     * proposed name box and the pan box\n     */\n    private fun getHeightScore(prediction: DetectionBox, panBox: DetectionBox): Float {\n        val mean = 0.7697886777933562f\n        val std = 0.16833893197497318f\n        val panHeight = panBox.rect.height()\n        val heightRatio = prediction.rect.height() / panHeight\n        return (-1 / 2f) * ((heightRatio - mean) / std).pow(2) * 4\n    }\n\n    private fun calculateNameWidthScore(nameBox: MergedBox): Float {\n        val mean = 8.616257541544856f\n        val std = 4.095034614992819f\n        val nameWidthRatio = nameBox.box.rect.width() / nameBox.box.rect.height()\n        var nameWidthScore = (-0.5f) * ((nameWidthRatio - mean) / std).pow(2)\n\n        // penalize names shorter than 6 characters\n        val shortNamePenalty = max(6 - nameWidthRatio, 0f)\n        nameWidthScore -= 10 * shortNamePenalty\n\n        // penalize deviations from 2 to 3 boxes\n        val numBoxPenalty = floor(abs(nameBox.subBoxes.size - 2.5f))\n        nameWidthScore -= 2 * numBoxPenalty\n\n        // penalize names longer than 25 characters\n        val longNamePenalty = max(nameWidthRatio - 25, 0f)\n        nameWidthScore -= 5 * longNamePenalty\n\n        return nameWidthScore\n    }\n\n    override suspend fun transformData(data: Input): Array<ByteBuffer> = arrayOf(data.textDetectImage.image.getData())\n\n    override suspend fun executeInference(\n        tfInterpreter: Interpreter,\n        data: Array<ByteBuffer>,\n    ): Map<Int, Array<Array<Array<FloatArray>>>> {\n        val mlOutput = mapOf(\n            0 to arrayOf(\n                Array(LAYER_1_SIZE.width) {\n                    Array(LAYER_1_SIZE.height) {\n                        FloatArray(DIM_Z)\n                    }\n                }\n            ),\n            1 to arrayOf(\n                Array(LAYER_2_SIZE.width) {\n                    Array(LAYER_2_SIZE.height) {\n                        FloatArray(DIM_Z)\n                    }\n                }\n            )\n        )\n\n        tfInterpreter.runForMultipleInputsOutputs(data, mlOutput)\n        return mlOutput\n    }\n\n    /**\n     * A factory for creating instances of this analyzer. This downloads the model from the web. If unable to download\n     * from the web, this will throw a [FileNotFoundException].\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    class Factory(\n        context: Context,\n        fetchedModel: FetchedData,\n        threads: Int = DEFAULT_THREADS\n    ) : TFLAnalyzerFactory<Input, Prediction, TextDetect>(context, fetchedModel) {\n        companion object {\n            private const val USE_GPU = false\n            private const val DEFAULT_THREADS = 1\n        }\n\n        override val tfOptions: Interpreter.Options = Interpreter\n            .Options()\n            .setUseNNAPI(USE_GPU && hasOpenGl31(context))\n            .setNumThreads(threads)\n\n        override suspend fun newInstance(): TextDetect? = createInterpreter()?.let { TextDetect(it) }\n    }\n\n    /**\n     * A fetcher for downloading model data.\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    class ModelFetcher(context: Context) : UpdatingModelWebFetcher(context) {\n        override val modelClass: String = \"text_detection\"\n        override val modelFrameworkVersion: Int = 1\n        override val defaultModelVersion: String = \"20.16\"\n        override val defaultModelFileName: String = \"dlnm.tflite\"\n        override val defaultModelHash: String = \"c84564bf856358fbb2995c962ef5dd4a892dcaa593b61bf540324475db26afef\"\n        override val defaultModelHashAlgorithm: String = \"SHA-256\"\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/ml/ssd/DetectionBox.kt",
    "content": "package com.getbouncer.scan.payment.ml.ssd\n\nimport android.graphics.RectF\nimport androidx.annotation.RestrictTo\n\n@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class DetectionBox(\n\n    /**\n     * The rectangle percentage of the original image.\n     */\n    val rect: RectF,\n\n    /**\n     * Confidence value that the label applies to the rectangle.\n     */\n    val confidence: Float,\n\n    /**\n     * The label for this box.\n     */\n    val label: Int\n)\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/ml/ssd/OcrPriorsGenerator.kt",
    "content": "package com.getbouncer.scan.payment.ml.ssd\n\nimport android.util.Size\nimport com.getbouncer.scan.framework.ml.ssd.SizeAndCenter\nimport com.getbouncer.scan.framework.ml.ssd.clampAll\nimport com.getbouncer.scan.framework.ml.ssd.sizeAndCenter\nimport kotlin.math.sqrt\n\nprivate const val NUMBER_OF_PRIORS = 3\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun combinePriors(trainedImageSize: Size): Array<SizeAndCenter> {\n    val priorsOne: Array<SizeAndCenter> =\n        generatePriors(\n            trainedImageSize = trainedImageSize,\n            featureMapSize = Size(38, 24),\n            shrinkage = Size(16, 16),\n            boxSizeMin = 14F,\n            boxSizeMax = 30F,\n            aspectRatio = 3F\n        )\n\n    val priorsTwo: Array<SizeAndCenter> =\n        generatePriors(\n            trainedImageSize = trainedImageSize,\n            featureMapSize = Size(19, 12),\n            shrinkage = Size(31, 31),\n            boxSizeMin = 30F,\n            boxSizeMax = 45F,\n            aspectRatio = 3F\n        )\n\n    return (priorsOne + priorsTwo).apply { forEach { it.clampAll(0F, 1F) } }\n}\n\nprivate fun generatePriors(\n    trainedImageSize: Size,\n    featureMapSize: Size,\n    shrinkage: Size,\n    boxSizeMin: Float,\n    boxSizeMax: Float,\n    aspectRatio: Float\n): Array<SizeAndCenter> {\n    val scaleWidth = trainedImageSize.width.toFloat() / shrinkage.width\n    val scaleHeight = trainedImageSize.height.toFloat() / shrinkage.height\n    val ratio = sqrt(aspectRatio)\n\n    fun generatePrior(column: Int, row: Int, sizeFactor: Float, ratio: Float) =\n        sizeAndCenter(\n            centerX = (column + 0.5F) / scaleWidth,\n            centerY = (row + 0.5F) / scaleHeight,\n            width = sizeFactor / trainedImageSize.width,\n            height = sizeFactor / trainedImageSize.height * ratio\n        )\n\n    return Array(featureMapSize.width * featureMapSize.height * NUMBER_OF_PRIORS) { index ->\n        val row = index / NUMBER_OF_PRIORS / featureMapSize.width\n        val column = (index / NUMBER_OF_PRIORS) % featureMapSize.width\n        when (index % NUMBER_OF_PRIORS) {\n            0 -> generatePrior(column, row, boxSizeMin, 1F)\n            1 -> generatePrior(column, row, sqrt(boxSizeMax * boxSizeMin), ratio)\n            else -> generatePrior(column, row, boxSizeMin, ratio)\n        }\n    }\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/ml/ssd/SSD.kt",
    "content": "package com.getbouncer.scan.payment.ml.ssd\n\nimport com.getbouncer.scan.framework.ml.hardNonMaximumSuppression\nimport com.getbouncer.scan.framework.ml.ssd.ClassifierScores\nimport com.getbouncer.scan.framework.ml.ssd.RectForm\nimport com.getbouncer.scan.framework.ml.ssd.toRectF\nimport com.getbouncer.scan.framework.util.filterByIndexes\nimport com.getbouncer.scan.framework.util.filteredIndexes\nimport com.getbouncer.scan.framework.util.transpose\nimport com.getbouncer.scan.payment.card.QUICK_READ_GROUP_LENGTH\nimport com.getbouncer.scan.payment.card.QUICK_READ_LENGTH\nimport kotlin.math.abs\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class OcrFeatureMapSizes(\n    val layerOneWidth: Int,\n    val layerOneHeight: Int,\n    val layerTwoWidth: Int,\n    val layerTwoHeight: Int\n)\n\n/**\n * The model outputs a particular location or a particular class of each prior before moving\n * on to the next prior. For instance, the model will output probabilities for background\n * class corresponding to all priors before outputting the probability of next class for the\n * first prior. This method serves to rearrange the output if you are using outputs from\n * multiple layers If you use outputs from single layer use the method defined above\n *\n * TODO: simplify this\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun rearrangeOCRArray(\n    locations: Array<FloatArray>,\n    featureMapSizes: OcrFeatureMapSizes,\n    numberOfPriors: Int,\n    locationsPerPrior: Int\n): Array<FloatArray> {\n    val totalLocationsForAllLayers =\n        featureMapSizes.layerOneWidth * featureMapSizes.layerOneHeight * numberOfPriors * locationsPerPrior +\n            featureMapSizes.layerTwoWidth * featureMapSizes.layerTwoHeight * numberOfPriors * locationsPerPrior\n    val rearranged = Array(1) { FloatArray(totalLocationsForAllLayers) }\n    val featureMapHeights = arrayOf(\n        featureMapSizes.layerOneHeight,\n        featureMapSizes.layerTwoHeight\n    )\n    val featureMapWidths = arrayOf(\n        featureMapSizes.layerOneWidth,\n        featureMapSizes.layerTwoWidth\n    )\n    val heightIterator = featureMapHeights.iterator()\n    val widthIterator = featureMapWidths.iterator()\n    var offset = 0\n\n    while (heightIterator.hasNext() && widthIterator.hasNext()) {\n        val height = heightIterator.next()\n        val width = widthIterator.next()\n        val totalNumberOfLocationsForThisLayer = height * width * numberOfPriors * locationsPerPrior\n        val stepsForLoop = height - 1\n        var j: Int\n        var i = 0\n        var step = 0\n        while (i < totalNumberOfLocationsForThisLayer) {\n            while (step < height) {\n                j = step\n                while (j < totalNumberOfLocationsForThisLayer - stepsForLoop + step) {\n                    rearranged[0][offset + i] = locations[0][offset + j]\n                    i++\n                    j += height\n                }\n                step++\n            }\n            offset += totalNumberOfLocationsForThisLayer\n        }\n    }\n    return rearranged\n}\n\n/**\n * Applies non-maximum suppression to each class. Picks out the remaining boxes, the class\n * probabilities for classes that are kept, and composes all the information.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun extractPredictions(\n    scores: Array<ClassifierScores>,\n    boxes: Array<RectForm>,\n    probabilityThreshold: Float,\n    intersectionOverUnionThreshold: Float,\n    limit: Int?,\n    classifierToLabel: (Int) -> Int = { it }\n): List<DetectionBox> {\n    val predictions = mutableListOf<DetectionBox>()\n\n    val classifiersScores = scores.transpose()\n\n    for (classifier in 1 until classifiersScores.size) { // skip the background classifier (index = 0)\n        val classifierScores = classifiersScores[classifier]\n        val filteredIndexes = classifierScores.filteredIndexes { it >= probabilityThreshold }\n\n        if (filteredIndexes.isNotEmpty()) {\n            val filteredScores = classifierScores.filterByIndexes(filteredIndexes)\n            val filteredBoxes = boxes.filterByIndexes(filteredIndexes)\n\n            val indexes =\n                hardNonMaximumSuppression(\n                    boxes = filteredBoxes,\n                    probabilities = filteredScores,\n                    iouThreshold = intersectionOverUnionThreshold,\n                    limit = limit\n                )\n            for (index in indexes) {\n                predictions.add(\n                    DetectionBox(\n                        rect = filteredBoxes[index].toRectF(),\n                        confidence = filteredScores[index],\n                        label = classifierToLabel(classifier)\n                    )\n                )\n            }\n        }\n    }\n\n    return predictions\n}\n\n/**\n * Determine if the number is displayed horizontally or both horizontally and vertically. We do this\n * by finding the median vertical coordinate center. If the number is displayed horizontally, the\n * deviation of all the number boxes and the median center should be minimal since they are laid on\n * roughly the same horizontal line. In this case we just need to sort from left to right to order\n * the number boxes. Additionally, we also filter out boxes that are outside the same horizontal\n * line. This is done to exclude information such as phone numbers or expiry. On the other hand, if\n * the aggregate deviation of the number box centers from the median center is above a threshold,\n * i.e. the number has both vertical and horizontal components we need to sort from left to right\n * and top to bottom to order the boxes according to the card number.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun determineLayoutAndFilter(detectedBoxes: List<DetectionBox>, verticalOffset: Float): List<DetectionBox> {\n    if (detectedBoxes.isEmpty()) {\n        return detectedBoxes\n    }\n\n    // calculate the median center and height of each digit in the image\n    val centers = detectedBoxes.map { it.rect.centerY() }.sorted()\n    val heights = detectedBoxes.map { it.rect.height() }.sorted()\n\n    val medianCenter = centers.elementAt(centers.size / 2)\n    val medianHeight = heights.elementAt(heights.size / 2)\n    val aggregateDeviation = centers.map { abs(it - medianCenter) }.sum()\n\n    if (aggregateDeviation > verticalOffset * medianHeight && detectedBoxes.size == QUICK_READ_LENGTH) {\n        val quickReadGroups = detectedBoxes\n            .sortedBy { it.rect.centerY() }\n            .chunked(QUICK_READ_GROUP_LENGTH)\n            .map { it.sortedBy { detectionBox -> detectionBox.rect.left } }\n\n        // Quick read groups should be in vertical blocks. Make sure the blocks are not horizontally laid out\n        if (quickReadGroups[1].first().rect.centerX() < quickReadGroups[0].last().rect.centerX() && quickReadGroups[1].last().rect.centerX() > quickReadGroups[0].first().rect.centerX()) {\n            return quickReadGroups.flatten()\n        }\n    }\n\n    return detectedBoxes\n        .sortedBy { it.rect.left }\n        .filter { abs(it.rect.centerY() - medianCenter) <= medianHeight }\n}\n"
  },
  {
    "path": "scan-payment/src/main/java/com/getbouncer/scan/payment/ml/yolo/Yolo.kt",
    "content": "package com.getbouncer.scan.payment.ml.yolo\n\nimport android.graphics.RectF\nimport android.util.Size\nimport com.getbouncer.scan.framework.ml.ssd.softMax\nimport com.getbouncer.scan.framework.util.indexOfMax\nimport com.getbouncer.scan.payment.ml.ssd.DetectionBox\nimport kotlin.math.exp\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun sigmoid(x: Float): Float = (1.0f / (1.0f + exp((-x))))\n\n/**\n * Takes a layer from the raw YOLO model output and performs post-processing on it,\n * returning a List<DetectionBox>\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun processYoloLayer(\n    layer: Array<Array<FloatArray>>,\n    anchors: Array<Pair<Int, Int>>,\n    imageSize: Size,\n    numClasses: Int,\n    confidenceThreshold: Float\n): List<DetectionBox> {\n    val results = mutableListOf<DetectionBox>()\n    for (i in layer.indices) for (j in layer[i].indices) for (k in 0..2) {\n        val offset = (numClasses + 5) * k\n        var confidence = sigmoid(layer[i][j][offset + 4])\n        val confidenceClasses = (0 until numClasses).map { layer[i][j][offset + 5 + it] }.toFloatArray()\n        confidenceClasses.softMax()\n\n        val objectId = confidenceClasses.indexOfMax() ?: continue\n        val maxClass = confidenceClasses[objectId]\n        confidence *= maxClass\n\n        if (confidence > confidenceThreshold) {\n            val x = (j + sigmoid(layer[i][j][offset])) / layer.size\n            val y = (i + sigmoid(layer[i][j][offset + 1])) / layer.size\n            val w =\n                exp(layer[i][j][offset + 2]) * anchors[k].first / imageSize.width\n            val h =\n                exp(layer[i][j][offset + 3]) * anchors[k].second / imageSize.height\n            val r = RectF(\n                x - w / 2,\n                y - h / 2,\n                x + w / 2,\n                y + h / 2\n            )\n            results.add(\n                DetectionBox(\n                    rect = r,\n                    confidence = confidence,\n                    label = objectId\n                )\n            )\n        }\n    }\n    return results\n}\n"
  },
  {
    "path": "scan-payment/src/test/java/com/getbouncer/scan/payment/card/PaymentCardTest.kt",
    "content": "package com.getbouncer.scan.payment.card\n\nimport androidx.test.filters.SmallTest\nimport org.junit.Before\nimport org.junit.Test\nimport java.util.Calendar\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFalse\nimport kotlin.test.assertTrue\n\nprivate const val SAMPLE_AMEX_PAN = \"340000000000009\"\nprivate const val SAMPLE_DINERS_CLUB_PAN_14 = \"36281412218285\"\nprivate const val SAMPLE_DINERS_CLUB_PAN_16 = \"3628141221828005\"\nprivate const val SAMPLE_DISCOVER_PAN = \"6011000000000004\"\nprivate const val SAMPLE_JCB_PAN = \"3528902605615800\"\nprivate const val SAMPLE_MASTERCARD_PAN = \"5500000000000004\"\nprivate const val SAMPLE_UNIONPAY_16_PAN = \"6212345678901232\"\nprivate const val SAMPLE_UNIONPAY_17_PAN = \"62123456789000003\"\nprivate const val SAMPLE_UNIONPAY_18_PAN = \"621234567890000002\"\nprivate const val SAMPLE_UNIONPAY_19_PAN = \"6212345678900000003\"\nprivate const val SAMPLE_VISA_PAN = \"4847186095118770\"\n\nprivate const val SAMPLE_AMEX_IIN = \"340000\"\nprivate const val SAMPLE_DINERS_CLUB_IIN = \"300000\"\nprivate const val SAMPLE_DISCOVER_IIN = \"601100\"\nprivate const val SAMPLE_JCB_IIN = \"352890\"\nprivate const val SAMPLE_MASTERCARD_IIN = \"550000\"\nprivate const val SAMPLE_UNIONPAY_IIN = \"621234\"\nprivate const val SAMPLE_VISA_IIN = \"411111\"\n\nprivate const val SAMPLE_AMEX_CVC = \"1234\"\nprivate const val SAMPLE_NORMAL_CVC = \"123\"\nprivate const val SAMPLE_INVALID_CVC = \"12\"\n\nprivate const val SAMPLE_CUSTOM_16_PAN = \"9900000000000101\"\nprivate const val SAMPLE_CUSTOM_17_PAN = \"99000000000000002\"\nprivate const val SAMPLE_CUSTOM_18_PAN = \"990000000000000903\"\nprivate const val SAMPLE_CUSTOM_19_PAN = \"9900000000000000804\"\nprivate const val SAMPLE_ADVANCED_CUSTOM_20_PAN = \"99100000000000000505\"\n\nprivate const val SAMPLE_CUSTOM_IIN = \"990023\"\nprivate const val SAMPLE_ADVANCED_CUSTOM_IIN = \"991456\"\n\nprivate const val SAMPLE_CUSTOM_CVC = \"123\"\nprivate const val SAMPLE_ADVANCED_CUSTOM_CVC = \"1234\"\n\nprivate val SAMPLE_CUSTOM_CARD_ISSUER = CardIssuer.Custom(\"Custom\")\nprivate val SAMPLE_ADVANCED_CUSTOM_CARD_ISSUER = CardIssuer.Custom(\"Advanced Custom\")\n\nclass PaymentCardTest {\n\n    @Before\n    fun addCardIssuers() {\n        supportCardIssuer(990000..990024, SAMPLE_CUSTOM_CARD_ISSUER, (16..19).toList(), listOf(3))\n        supportCardIssuer(991000..991999, SAMPLE_ADVANCED_CUSTOM_CARD_ISSUER, listOf(20), listOf(4))\n        addFormatPan(SAMPLE_CUSTOM_CARD_ISSUER, 16, 4, 3, 5, 4)\n        addFormatPan(SAMPLE_CUSTOM_CARD_ISSUER, 17, 4, 4, 5, 4)\n        addFormatPan(SAMPLE_CUSTOM_CARD_ISSUER, 18, 4, 5, 5, 4)\n        addFormatPan(SAMPLE_CUSTOM_CARD_ISSUER, 19, 5, 5, 5, 4)\n        addFormatPan(SAMPLE_ADVANCED_CUSTOM_CARD_ISSUER, 20, 5, 5, 5, 5)\n    }\n\n    @Test\n    @SmallTest\n    fun getCardIssuer() {\n        assertEquals(CardIssuer.AmericanExpress, getCardIssuer(SAMPLE_AMEX_PAN))\n        assertEquals(CardIssuer.DinersClub, getCardIssuer(SAMPLE_DINERS_CLUB_PAN_14))\n        assertEquals(CardIssuer.DinersClub, getCardIssuer(SAMPLE_DINERS_CLUB_PAN_16))\n        assertEquals(CardIssuer.Discover, getCardIssuer(SAMPLE_DISCOVER_PAN))\n        assertEquals(CardIssuer.JCB, getCardIssuer(SAMPLE_JCB_PAN))\n        assertEquals(CardIssuer.MasterCard, getCardIssuer(SAMPLE_MASTERCARD_PAN))\n        assertEquals(CardIssuer.UnionPay, getCardIssuer(SAMPLE_UNIONPAY_16_PAN))\n        assertEquals(CardIssuer.UnionPay, getCardIssuer(SAMPLE_UNIONPAY_17_PAN))\n        assertEquals(CardIssuer.UnionPay, getCardIssuer(SAMPLE_UNIONPAY_18_PAN))\n        assertEquals(CardIssuer.UnionPay, getCardIssuer(SAMPLE_UNIONPAY_19_PAN))\n        assertEquals(CardIssuer.Visa, getCardIssuer(SAMPLE_VISA_PAN))\n        assertEquals(SAMPLE_CUSTOM_CARD_ISSUER, getCardIssuer(SAMPLE_CUSTOM_16_PAN))\n        assertEquals(SAMPLE_CUSTOM_CARD_ISSUER, getCardIssuer(SAMPLE_CUSTOM_17_PAN))\n        assertEquals(SAMPLE_CUSTOM_CARD_ISSUER, getCardIssuer(SAMPLE_CUSTOM_18_PAN))\n        assertEquals(SAMPLE_CUSTOM_CARD_ISSUER, getCardIssuer(SAMPLE_CUSTOM_19_PAN))\n        assertEquals(SAMPLE_ADVANCED_CUSTOM_CARD_ISSUER, getCardIssuer(SAMPLE_ADVANCED_CUSTOM_20_PAN))\n    }\n\n    @Test\n    @SmallTest\n    fun isValidPan() {\n        assertTrue { isValidPan(SAMPLE_AMEX_PAN) }\n        assertTrue { isValidPan(SAMPLE_DINERS_CLUB_PAN_14) }\n        assertTrue { isValidPan(SAMPLE_DINERS_CLUB_PAN_16) }\n        assertTrue { isValidPan(SAMPLE_DISCOVER_PAN) }\n        assertTrue { isValidPan(SAMPLE_JCB_PAN) }\n        assertTrue { isValidPan(SAMPLE_MASTERCARD_PAN) }\n        assertTrue { isValidPan(SAMPLE_UNIONPAY_16_PAN) }\n        assertTrue { isValidPan(SAMPLE_UNIONPAY_17_PAN) }\n        assertTrue { isValidPan(SAMPLE_UNIONPAY_18_PAN) }\n        assertTrue { isValidPan(SAMPLE_UNIONPAY_19_PAN) }\n        assertTrue { isValidPan(SAMPLE_VISA_PAN) }\n        assertTrue { isValidPan(SAMPLE_CUSTOM_16_PAN) }\n        assertTrue { isValidPan(SAMPLE_CUSTOM_17_PAN) }\n        assertTrue { isValidPan(SAMPLE_CUSTOM_18_PAN) }\n        assertTrue { isValidPan(SAMPLE_CUSTOM_19_PAN) }\n        assertTrue { isValidPan(SAMPLE_ADVANCED_CUSTOM_20_PAN) }\n    }\n\n    @Test\n    @SmallTest\n    fun isValidIin() {\n        assertTrue { isValidIin(SAMPLE_AMEX_IIN) }\n        assertTrue { isValidIin(SAMPLE_DINERS_CLUB_IIN) }\n        assertTrue { isValidIin(SAMPLE_DISCOVER_IIN) }\n        assertTrue { isValidIin(SAMPLE_JCB_IIN) }\n        assertTrue { isValidIin(SAMPLE_MASTERCARD_IIN) }\n        assertTrue { isValidIin(SAMPLE_UNIONPAY_IIN) }\n        assertTrue { isValidIin(SAMPLE_VISA_IIN) }\n        assertTrue { isValidIin(SAMPLE_CUSTOM_IIN) }\n        assertTrue { isValidIin(SAMPLE_ADVANCED_CUSTOM_IIN) }\n    }\n\n    @Test\n    @SmallTest\n    fun isValidCvc() {\n        assertTrue { isValidCvc(SAMPLE_AMEX_CVC, CardIssuer.AmericanExpress) }\n        assertTrue { isValidCvc(SAMPLE_NORMAL_CVC, CardIssuer.Visa) }\n        assertTrue { isValidCvc(SAMPLE_CUSTOM_CVC, SAMPLE_CUSTOM_CARD_ISSUER) }\n        assertTrue { isValidCvc(SAMPLE_ADVANCED_CUSTOM_CVC, SAMPLE_ADVANCED_CUSTOM_CARD_ISSUER) }\n        assertFalse { isValidCvc(SAMPLE_AMEX_CVC, CardIssuer.MasterCard) }\n        assertTrue { isValidCvc(SAMPLE_NORMAL_CVC, CardIssuer.AmericanExpress) }\n        assertFalse { isValidCvc(SAMPLE_INVALID_CVC, null) }\n        assertTrue { isValidCvc(SAMPLE_NORMAL_CVC, null) }\n        assertTrue { isValidCvc(SAMPLE_AMEX_CVC, null) }\n        assertFalse { isValidCvc(\"a12\", CardIssuer.Visa) }\n    }\n\n    @Test\n    @SmallTest\n    fun isValidExpiry() {\n        val expDay = \"01\"\n        val expMonth = \"02\"\n        val expYear = \"2032\"\n\n        assertTrue { isValidExpiry(expDay, expMonth, expYear) }\n        assertTrue { isValidExpiry(null, expMonth, expYear) }\n    }\n\n    @Test\n    @SmallTest\n    fun isValidExpiry_leapYear() {\n        val expDay = \"29\"\n        val expMonth = \"2\"\n        val expYear = \"2024\"\n\n        assertTrue { isValidExpiry(expDay, expMonth, expYear) }\n    }\n\n    @Test\n    @SmallTest\n    fun isValidExpiry_nonLeapYear() {\n        val expDay = \"29\"\n        val expMonth = \"2\"\n        val expYear = \"2023\"\n\n        assertFalse { isValidExpiry(expDay, expMonth, expYear) }\n    }\n\n    @Test\n    @SmallTest\n    fun isValidExpiry_pastDate() {\n        val expDay = \"10\"\n        val expMonth = \"25\"\n        val expYear = \"2019\"\n\n        assertFalse { isValidExpiry(expDay, expMonth, expYear) }\n    }\n\n    @Test\n    @SmallTest\n    fun isValidExpiry_pastDay() {\n        val cal = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -1) }\n        val expDay = cal.get(Calendar.DAY_OF_MONTH).toString()\n        val expMonth = (cal.get(Calendar.MONTH) + 1).toString() // Calendar months are 0-based.\n        val expYear = cal.get(Calendar.YEAR).toString()\n\n        assertFalse { isValidExpiry(expDay, expMonth, expYear) }\n    }\n\n    @Test\n    @SmallTest\n    fun isValidExpiry_pastMonth() {\n        val cal = Calendar.getInstance().apply { add(Calendar.MONTH, -1) }\n        val expDay = cal.get(Calendar.DAY_OF_MONTH).toString()\n        val expMonth = (cal.get(Calendar.MONTH) + 1).toString() // Calendar months are 0-based.\n        val expYear = cal.get(Calendar.YEAR).toString()\n\n        assertFalse { isValidExpiry(expDay, expMonth, expYear) }\n    }\n\n    @Test\n    @SmallTest\n    fun isValidExpiry_pastYear() {\n        val cal = Calendar.getInstance().apply { add(Calendar.YEAR, -1) }\n        val expDay = cal.get(Calendar.DAY_OF_MONTH).toString()\n        val expMonth = (cal.get(Calendar.MONTH) + 1).toString() // Calendar months are 0-based.\n        val expYear = cal.get(Calendar.YEAR).toString()\n\n        assertFalse { isValidExpiry(expDay, expMonth, expYear) }\n    }\n\n    @Test\n    @SmallTest\n    fun formatPan() {\n        assertEquals(\"3400 000000 00009\", formatPan(SAMPLE_AMEX_PAN))\n        assertEquals(\"3628 141221 8285\", formatPan(SAMPLE_DINERS_CLUB_PAN_14))\n        assertEquals(\"3628 1412 2182 8005\", formatPan(SAMPLE_DINERS_CLUB_PAN_16))\n        assertEquals(\"6011 0000 0000 0004\", formatPan(SAMPLE_DISCOVER_PAN))\n        assertEquals(\"3528 9026 0561 5800\", formatPan(SAMPLE_JCB_PAN))\n        assertEquals(\"5500 0000 0000 0004\", formatPan(SAMPLE_MASTERCARD_PAN))\n        assertEquals(\"6212 3456 7890 1232\", formatPan(SAMPLE_UNIONPAY_16_PAN))\n        assertEquals(\"6212 3456 7890 00003\", formatPan(SAMPLE_UNIONPAY_17_PAN))\n        assertEquals(\"6212 3456 7890 000002\", formatPan(SAMPLE_UNIONPAY_18_PAN))\n        assertEquals(\"621234 5678900000003\", formatPan(SAMPLE_UNIONPAY_19_PAN))\n        assertEquals(\"4847 1860 9511 8770\", formatPan(SAMPLE_VISA_PAN))\n        assertEquals(\"9900 000 00000 0101\", formatPan(SAMPLE_CUSTOM_16_PAN))\n        assertEquals(\"9900 0000 00000 0002\", formatPan(SAMPLE_CUSTOM_17_PAN))\n        assertEquals(\"9900 00000 00000 0903\", formatPan(SAMPLE_CUSTOM_18_PAN))\n        assertEquals(\"99000 00000 00000 0804\", formatPan(SAMPLE_CUSTOM_19_PAN))\n        assertEquals(\"99100 00000 00000 00505\", formatPan(SAMPLE_ADVANCED_CUSTOM_20_PAN))\n        assertEquals(\"1234 5678 9012 3456\", formatPan(\"1234567890123456\"))\n    }\n\n    @Test\n    @SmallTest\n    fun formatIssuer() {\n        assertEquals(\"American Express\", formatIssuer(CardIssuer.AmericanExpress))\n        assertEquals(\"Diners Club\", formatIssuer(CardIssuer.DinersClub))\n        assertEquals(\"Discover\", formatIssuer(CardIssuer.Discover))\n        assertEquals(\"JCB\", formatIssuer(CardIssuer.JCB))\n        assertEquals(\"MasterCard\", formatIssuer(CardIssuer.MasterCard))\n        assertEquals(\"UnionPay\", formatIssuer(CardIssuer.UnionPay))\n        assertEquals(\"Unknown\", formatIssuer(CardIssuer.Unknown))\n        assertEquals(\"Visa\", formatIssuer(CardIssuer.Visa))\n        assertEquals(\"Custom\", formatIssuer(SAMPLE_CUSTOM_CARD_ISSUER))\n        assertEquals(\"Advanced Custom\", formatIssuer(SAMPLE_ADVANCED_CUSTOM_CARD_ISSUER))\n    }\n\n    @Test\n    @SmallTest\n    fun formatExpiry() {\n        assertEquals(\"01/02/03\", formatExpiry(\"01\", \"02\", \"03\"))\n        assertEquals(\"01/02/03\", formatExpiry(\"1\", \"02\", \"03\"))\n        assertEquals(\"01/02/03\", formatExpiry(\"1\", \"2\", \"03\"))\n        assertEquals(\"01/02/03\", formatExpiry(\"1\", \"2\", \"3\"))\n        assertEquals(\"01/02/03\", formatExpiry(\"1\", \"2\", \"2003\"))\n        assertEquals(\"02/03\", formatExpiry(null, \"02\", \"03\"))\n    }\n}\n"
  },
  {
    "path": "scan-payment-full/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "scan-payment-full/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\napply plugin: 'kotlinx-serialization'\n\nandroid {\n    compileSdkVersion 30\n    buildToolsVersion '30.0.3'\n\n    defaultConfig {\n        minSdkVersion 21\n        targetSdkVersion 30\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles \"consumer-rules.pro\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile(\"proguard-android-optimize.txt\"), \"proguard-rules.pro\"\n        }\n    }\n\n    testOptions {\n        unitTests.includeAndroidResources = true\n    }\n\n    lintOptions {\n        enable \"Interoperability\"\n    }\n\n    packagingOptions {\n        pickFirst 'META-INF/AL2.0'\n        pickFirst 'META-INF/LGPL2.1'\n    }\n\n    aaptOptions {\n        noCompress \"tflite\"\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: \"libs\", include: [\"*.jar\"])\n    api project(\":scan-framework\")\n    api project(\":scan-payment\")\n\n    implementation \"androidx.core:core-ktx:[1.3.1,1.6.0]\"\n}\n\ndependencies {\n    testImplementation \"androidx.test:core:1.4.0\"\n    testImplementation \"androidx.test:runner:1.4.0\"\n    testImplementation \"junit:junit:4.13.2\"\n    testImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n}\n\ndependencies {\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.4.0\"\n    androidTestImplementation \"androidx.test.ext:junit:1.1.3\"\n    androidTestImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n    androidTestImplementation \"org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1\"\n}\n\napply from: \"deploy.gradle\"\n"
  },
  {
    "path": "scan-payment-full/deploy.gradle",
    "content": "apply plugin: 'maven-publish'\napply plugin: 'org.jetbrains.dokka'\napply plugin: 'signing'\n\ntask androidSourcesJar(type: Jar) {\n    archiveClassifier.set('sources')\n    if (project.plugins.findPlugin(\"com.android.library\")) {\n        // Android library\n        from android.sourceSets.main.java.srcDirs\n        from android.sourceSets.main.kotlin.srcDirs\n    } else {\n        // Pure kotlin library\n        from sourceSets.main.java.srcDirs\n        from sourceSets.main.kotlin.srcDirs\n    }\n}\n\ntasks.withType(dokkaHtmlPartial.getClass()).configureEach {\n    pluginsMapConfiguration.set(\n            [\"org.jetbrains.dokka.base.DokkaBase\": \"\"\"{ \"separateInheritedMembers\": true}\"\"\"]\n    )\n}\n\ntask javadocJar(type: Jar, dependsOn: dokkaJavadoc) {\n    archiveClassifier.set('javadoc')\n    from dokkaJavadoc.outputDirectory\n}\n\nartifacts {\n    archives androidSourcesJar\n    archives javadocJar\n}\n\next[\"signing.keyId\"] = ''\next[\"signing.password\"] = ''\next[\"signing.secretKeyRingFile\"] = ''\n\next[\"ossrhUsername\"] = ''\next[\"ossrhPassword\"] = ''\next[\"sonatypeStagingProfileId\"] = ''\n\next {\n\n    libraryDescription = 'This library provides the framework for scanning payment cards'\n\n    siteUrl = 'https://getbouncer.com'\n\n    scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'\n    scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'\n    scmUrl = 'https://github.com/getbouncer/cardscan-android'\n\n    licenseName = 'bouncer-free-1'\n    licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'\n\n    developerId = 'getbouncer'\n    developerName = 'Bouncer Technologies'\n    developerEmail = 'bouncer-support@stripe.com'\n\n    publishGroupId = 'com.getbouncer'\n    publishArtifactId = 'scan-payment-full'\n    publishVersion = version\n}\n\ngroup = publishGroupId\nversion = publishVersion\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    Properties p = new Properties()\n    new FileInputStream(secretPropsFile).withCloseable { is ->\n        p.load(is)\n    }\n    p.each { name, value ->\n        ext[name] = value\n    }\n} else {\n    ext[\"signing.keyId\"] = System.getenv('SIGNING_KEY_ID')\n    ext[\"signing.password\"] = System.getenv('SIGNING_PASSWORD')\n    ext[\"signing.secretKeyRingFile\"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')\n\n    ext[\"ossrhUsername\"] = System.getenv('OSSRH_USERNAME')\n    ext[\"ossrhPassword\"] = System.getenv('OSSRH_PASSWORD')\n\n    ext[\"sonatypeStagingProfileId\"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')\n}\n\npublishing {\n    publications {\n        release(MavenPublication) {\n            groupId publishGroupId\n            artifactId publishArtifactId\n            version publishVersion\n\n            // Two artifacts, the `aar` (or `jar`) and the sources\n            if (project.plugins.findPlugin(\"com.android.library\")) {\n                artifact(\"$buildDir/outputs/aar/${project.getName()}-release.aar\")\n            } else {\n                artifact(\"$buildDir/libs/${project.getName()}-${version}.jar\")\n            }\n            artifact androidSourcesJar\n\n            pom {\n                name = publishArtifactId\n                description = libraryDescription\n                url = siteUrl\n                licenses {\n                    license {\n                        name = licenseName\n                        url = licenseUrl\n                    }\n                }\n                developers {\n                    developer {\n                        id = developerId\n                        name = developerName\n                        email = developerEmail\n                    }\n                }\n                scm {\n                    connection = scmConnection\n                    developerConnection = scmDeveloperConnection\n                    url = scmUrl\n                }\n                // A slightly hacky fix so that your POM will include any transitive dependencies\n                // that your library builds upon\n                withXml {\n                    def dependenciesNode = asNode().appendNode('dependencies')\n\n                    project.configurations.implementation.allDependencies.each {\n                        if (it.group != null && it.version != null) {\n                            def dependencyNode = dependenciesNode.appendNode('dependency')\n                            dependencyNode.appendNode('groupId', it.group)\n                            dependencyNode.appendNode('artifactId', it.name)\n                            dependencyNode.appendNode('version', it.version)\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // The repository to publish to, Sonatype/MavenCentral\n    repositories {\n        maven {\n            // This is an arbitrary name, you may also use \"mavencentral\" or\n            // any other name that's descriptive for you\n            name = \"sonatype\"\n            url = \"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/\"\n            credentials {\n                username ossrhUsername\n                password ossrhPassword\n            }\n        }\n    }\n}\n\nsigning {\n    sign publishing.publications\n}\n"
  },
  {
    "path": "scan-payment-full/src/androidTest/java/com/getbouncer/scan/payment/ml/CardDetectTest.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport androidx.core.graphics.drawable.toBitmap\nimport androidx.test.filters.MediumTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.getbouncer.scan.framework.Stats\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.UpdatingResourceFetcher\nimport com.getbouncer.scan.framework.image.size\nimport com.getbouncer.scan.framework.util.toRect\nimport com.getbouncer.scan.payment.full.test.R\nimport kotlinx.coroutines.runBlocking\nimport org.junit.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertTrue\n\nclass CardDetectTest {\n    private val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n    private val testContext = InstrumentationRegistry.getInstrumentation().context\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun cardDetect_pan() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.card_pan, null).toBitmap()\n        val fetcher = CardDetectModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertTrue(fetcher is UpdatingResourceFetcher)\n        fetcher.clearCache()\n\n        val fetchedData = fetcher.fetchData(forImmediateUse = true, isOptional = false) // use `forImmediateUse = true` to force the hash to match\n        assertEquals(fetcher.hash, fetchedData.modelHash)\n\n        val model = CardDetect.Factory(appContext, fetchedData).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            CardDetect.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        assertEquals(CardDetect.Prediction.Side.PAN, prediction.side)\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun cardDetect_noPan() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.card_no_pan, null).toBitmap()\n        val fetcher = CardDetectModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertTrue(fetcher is UpdatingResourceFetcher)\n        fetcher.clearCache()\n\n        val fetchedData = fetcher.fetchData(forImmediateUse = true, isOptional = false) // use `forImmediateUse = true` to force the hash to match\n        assertEquals(fetcher.hash, fetchedData.modelHash)\n\n        val model = CardDetect.Factory(appContext, fetchedData).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            CardDetect.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        assertEquals(CardDetect.Prediction.Side.NO_PAN, prediction.side)\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun cardDetect_noCard() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.card_no_card, null).toBitmap()\n        val fetcher = CardDetectModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertTrue(fetcher is UpdatingResourceFetcher)\n        fetcher.clearCache()\n\n        val fetchedData = fetcher.fetchData(forImmediateUse = true, isOptional = false) // use `forImmediateUse = true` to force the hash to match\n        assertEquals(fetcher.hash, fetchedData.modelHash)\n\n        val model = CardDetect.Factory(appContext, fetchedData).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            CardDetect.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        assertEquals(CardDetect.Prediction.Side.NO_CARD, prediction.side)\n    }\n}\n"
  },
  {
    "path": "scan-payment-full/src/androidTest/java/com/getbouncer/scan/payment/ml/SSDOcrTest.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport androidx.core.graphics.drawable.toBitmap\nimport androidx.test.filters.MediumTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.Stats\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.UpdatingResourceFetcher\nimport com.getbouncer.scan.framework.image.size\nimport com.getbouncer.scan.framework.util.toRect\nimport com.getbouncer.scan.payment.full.test.R\nimport kotlinx.coroutines.runBlocking\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertTrue\n\nclass SSDOcrTest {\n    private val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n    private val testContext = InstrumentationRegistry.getInstrumentation().context\n\n    @Before\n    fun before() {\n        Config.apiKey = \"qOJ_fF-WLDMbG05iBq5wvwiTNTmM2qIn\"\n    }\n\n    @After\n    fun after() {\n        Config.apiKey = null\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun resourceModelExecution_works() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.ocr_card_numbers, null).toBitmap()\n        val fetcher = SSDOcrModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertTrue(fetcher is UpdatingResourceFetcher)\n        fetcher.clearCache()\n\n        val model = SSDOcr.Factory(appContext, fetcher.fetchData(forImmediateUse = false, isOptional = false)).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            SSDOcr.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        assertEquals(\"3023334877861104\", prediction.pan)\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun resourceModelExecution_worksQR() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.ocr_card_numbers_qr, null).toBitmap()\n        val fetcher = SSDOcrModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertTrue(fetcher is UpdatingResourceFetcher)\n        fetcher.clearCache()\n\n        val model = SSDOcr.Factory(appContext, fetcher.fetchData(forImmediateUse = false, isOptional = false)).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            SSDOcr.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        assertEquals(\"4242424242424242\", prediction.pan)\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun resourceModelExecution_worksRepeatedly() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.ocr_card_numbers, null).toBitmap()\n        val fetcher = SSDOcrModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertTrue(fetcher is UpdatingResourceFetcher)\n        fetcher.clearCache()\n\n        val model = SSDOcr.Factory(appContext, fetcher.fetchData(forImmediateUse = true, isOptional = false)).newInstance()\n        assertNotNull(model)\n\n        val prediction1 = model.analyze(\n            SSDOcr.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        val prediction2 = model.analyze(\n            SSDOcr.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction1)\n        assertEquals(\"3023334877861104\", prediction1.pan)\n\n        assertNotNull(prediction2)\n        assertEquals(\"3023334877861104\", prediction2.pan)\n    }\n}\n"
  },
  {
    "path": "scan-payment-full/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.getbouncer.scan.payment.full\" />\n"
  },
  {
    "path": "scan-payment-minimal/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "scan-payment-minimal/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\napply plugin: 'kotlinx-serialization'\n\nandroid {\n    compileSdkVersion 30\n    buildToolsVersion '30.0.3'\n\n    defaultConfig {\n        minSdkVersion 21\n        targetSdkVersion 30\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles \"consumer-rules.pro\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile(\"proguard-android-optimize.txt\"), \"proguard-rules.pro\"\n        }\n    }\n\n    testOptions {\n        unitTests.includeAndroidResources = true\n    }\n\n    lintOptions {\n        enable \"Interoperability\"\n    }\n\n    packagingOptions {\n        pickFirst 'META-INF/AL2.0'\n        pickFirst 'META-INF/LGPL2.1'\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: \"libs\", include: [\"*.jar\"])\n    api project(\":scan-framework\")\n    api project(\":scan-payment\")\n\n    implementation \"androidx.core:core-ktx:[1.3.1,1.6.0]\"\n}\n\ndependencies {\n    testImplementation \"androidx.test:core:1.4.0\"\n    testImplementation \"androidx.test:runner:1.4.0\"\n    testImplementation \"junit:junit:4.13.2\"\n    testImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n}\n\ndependencies {\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.4.0\"\n    androidTestImplementation \"androidx.test.ext:junit:1.1.3\"\n    androidTestImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n    androidTestImplementation \"org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1\"\n}\n\napply from: \"deploy.gradle\"\n"
  },
  {
    "path": "scan-payment-minimal/deploy.gradle",
    "content": "apply plugin: 'maven-publish'\napply plugin: 'org.jetbrains.dokka'\napply plugin: 'signing'\n\ntask androidSourcesJar(type: Jar) {\n    archiveClassifier.set('sources')\n    if (project.plugins.findPlugin(\"com.android.library\")) {\n        // Android library\n        from android.sourceSets.main.java.srcDirs\n        from android.sourceSets.main.kotlin.srcDirs\n    } else {\n        // Pure kotlin library\n        from sourceSets.main.java.srcDirs\n        from sourceSets.main.kotlin.srcDirs\n    }\n}\n\ntasks.withType(dokkaHtmlPartial.getClass()).configureEach {\n    pluginsMapConfiguration.set(\n            [\"org.jetbrains.dokka.base.DokkaBase\": \"\"\"{ \"separateInheritedMembers\": true}\"\"\"]\n    )\n}\n\ntask javadocJar(type: Jar, dependsOn: dokkaJavadoc) {\n    archiveClassifier.set('javadoc')\n    from dokkaJavadoc.outputDirectory\n}\n\nartifacts {\n    archives androidSourcesJar\n    archives javadocJar\n}\n\next[\"signing.keyId\"] = ''\next[\"signing.password\"] = ''\next[\"signing.secretKeyRingFile\"] = ''\n\next[\"ossrhUsername\"] = ''\next[\"ossrhPassword\"] = ''\next[\"sonatypeStagingProfileId\"] = ''\n\next {\n\n    libraryDescription = 'This library provides the framework for scanning payment cards'\n\n    siteUrl = 'https://getbouncer.com'\n\n    scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'\n    scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'\n    scmUrl = 'https://github.com/getbouncer/cardscan-android'\n\n    licenseName = 'bouncer-free-1'\n    licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'\n\n    developerId = 'getbouncer'\n    developerName = 'Bouncer Technologies'\n    developerEmail = 'bouncer-support@stripe.com'\n\n    publishGroupId = 'com.getbouncer'\n    publishArtifactId = 'scan-payment-minimal'\n    publishVersion = version\n}\n\ngroup = publishGroupId\nversion = publishVersion\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    Properties p = new Properties()\n    new FileInputStream(secretPropsFile).withCloseable { is ->\n        p.load(is)\n    }\n    p.each { name, value ->\n        ext[name] = value\n    }\n} else {\n    ext[\"signing.keyId\"] = System.getenv('SIGNING_KEY_ID')\n    ext[\"signing.password\"] = System.getenv('SIGNING_PASSWORD')\n    ext[\"signing.secretKeyRingFile\"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')\n\n    ext[\"ossrhUsername\"] = System.getenv('OSSRH_USERNAME')\n    ext[\"ossrhPassword\"] = System.getenv('OSSRH_PASSWORD')\n\n    ext[\"sonatypeStagingProfileId\"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')\n}\n\npublishing {\n    publications {\n        release(MavenPublication) {\n            groupId publishGroupId\n            artifactId publishArtifactId\n            version publishVersion\n\n            // Two artifacts, the `aar` (or `jar`) and the sources\n            if (project.plugins.findPlugin(\"com.android.library\")) {\n                artifact(\"$buildDir/outputs/aar/${project.getName()}-release.aar\")\n            } else {\n                artifact(\"$buildDir/libs/${project.getName()}-${version}.jar\")\n            }\n            artifact androidSourcesJar\n\n            pom {\n                name = publishArtifactId\n                description = libraryDescription\n                url = siteUrl\n                licenses {\n                    license {\n                        name = licenseName\n                        url = licenseUrl\n                    }\n                }\n                developers {\n                    developer {\n                        id = developerId\n                        name = developerName\n                        email = developerEmail\n                    }\n                }\n                scm {\n                    connection = scmConnection\n                    developerConnection = scmDeveloperConnection\n                    url = scmUrl\n                }\n                // A slightly hacky fix so that your POM will include any transitive dependencies\n                // that your library builds upon\n                withXml {\n                    def dependenciesNode = asNode().appendNode('dependencies')\n\n                    project.configurations.implementation.allDependencies.each {\n                        if (it.group != null && it.version != null) {\n                            def dependencyNode = dependenciesNode.appendNode('dependency')\n                            dependencyNode.appendNode('groupId', it.group)\n                            dependencyNode.appendNode('artifactId', it.name)\n                            dependencyNode.appendNode('version', it.version)\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // The repository to publish to, Sonatype/MavenCentral\n    repositories {\n        maven {\n            // This is an arbitrary name, you may also use \"mavencentral\" or\n            // any other name that's descriptive for you\n            name = \"sonatype\"\n            url = \"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/\"\n            credentials {\n                username ossrhUsername\n                password ossrhPassword\n            }\n        }\n    }\n}\n\nsigning {\n    sign publishing.publications\n}\n"
  },
  {
    "path": "scan-payment-minimal/src/androidTest/java/com/getbouncer/scan/payment/ml/CardDetectTest.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport androidx.core.graphics.drawable.toBitmap\nimport androidx.test.filters.MediumTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.getbouncer.scan.framework.Stats\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.UpdatingResourceFetcher\nimport com.getbouncer.scan.framework.image.size\nimport com.getbouncer.scan.framework.util.toRect\nimport com.getbouncer.scan.payment.minimal.test.R\nimport kotlinx.coroutines.runBlocking\nimport org.junit.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertTrue\n\nclass CardDetectTest {\n    private val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n    private val testContext = InstrumentationRegistry.getInstrumentation().context\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun cardDetect_pan() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.card_pan, null).toBitmap()\n        val fetcher = CardDetectModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertTrue(fetcher is UpdatingResourceFetcher)\n        fetcher.clearCache()\n\n        val fetchedData = fetcher.fetchData(forImmediateUse = true, isOptional = false) // use `forImmediateUse = true` to force the hash to match\n        assertEquals(fetcher.hash, fetchedData.modelHash)\n\n        val model = CardDetect.Factory(appContext, fetchedData).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            CardDetect.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        assertEquals(CardDetect.Prediction.Side.PAN, prediction.side)\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun cardDetect_noPan() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.card_no_pan, null).toBitmap()\n        val fetcher = CardDetectModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertTrue(fetcher is UpdatingResourceFetcher)\n        fetcher.clearCache()\n\n        val fetchedData = fetcher.fetchData(forImmediateUse = true, isOptional = false) // use `forImmediateUse = true` to force the hash to match\n        assertEquals(fetcher.hash, fetchedData.modelHash)\n\n        val model = CardDetect.Factory(appContext, fetchedData).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            CardDetect.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        assertEquals(CardDetect.Prediction.Side.NO_PAN, prediction.side)\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun cardDetect_noCard() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.card_no_card, null).toBitmap()\n        val fetcher = CardDetectModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertTrue(fetcher is UpdatingResourceFetcher)\n        fetcher.clearCache()\n\n        val fetchedData = fetcher.fetchData(forImmediateUse = true, isOptional = false) // use `forImmediateUse = true` to force the hash to match\n        assertEquals(fetcher.hash, fetchedData.modelHash)\n\n        val model = CardDetect.Factory(appContext, fetchedData).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            CardDetect.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        assertEquals(CardDetect.Prediction.Side.NO_CARD, prediction.side)\n    }\n}\n"
  },
  {
    "path": "scan-payment-minimal/src/androidTest/java/com/getbouncer/scan/payment/ml/SSDOcrTest.kt",
    "content": "package com.getbouncer.scan.payment.ml\n\nimport androidx.core.graphics.drawable.toBitmap\nimport androidx.test.filters.MediumTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.Stats\nimport com.getbouncer.scan.framework.TrackedImage\nimport com.getbouncer.scan.framework.UpdatingResourceFetcher\nimport com.getbouncer.scan.framework.image.size\nimport com.getbouncer.scan.framework.util.toRect\nimport com.getbouncer.scan.payment.minimal.test.R\nimport kotlinx.coroutines.runBlocking\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertTrue\n\nclass SSDOcrTest {\n    private val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n    private val testContext = InstrumentationRegistry.getInstrumentation().context\n\n    @Before\n    fun before() {\n        Config.apiKey = \"qOJ_fF-WLDMbG05iBq5wvwiTNTmM2qIn\"\n    }\n\n    @After\n    fun after() {\n        Config.apiKey = null\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun resourceModelExecution_works() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.ocr_card_numbers, null).toBitmap()\n        val fetcher = SSDOcrModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertTrue(fetcher is UpdatingResourceFetcher)\n        fetcher.clearCache()\n\n        val model = SSDOcr.Factory(appContext, fetcher.fetchData(forImmediateUse = false, isOptional = false)).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            SSDOcr.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        // TODO: this is inconsistent due to the low quality of the minimal model, and the OCR result will change from run to run.\n        // assertEquals(\"3023334877861104\", prediction.pan)\n\n        Unit\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun resourceModelExecution_worksQR() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.ocr_card_numbers_qr, null).toBitmap()\n        val fetcher = SSDOcrModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertTrue(fetcher is UpdatingResourceFetcher)\n        fetcher.clearCache()\n\n        val model = SSDOcr.Factory(appContext, fetcher.fetchData(forImmediateUse = false, isOptional = false)).newInstance()\n        assertNotNull(model)\n\n        val prediction = model.analyze(\n            SSDOcr.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction)\n        assertEquals(\"4242424242424242\", prediction.pan)\n    }\n\n    /**\n     * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with\n     * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or\n     * withContext(Dispatchers.Default).\n     *\n     * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.\n     */\n    @Test\n    @MediumTest\n    fun resourceModelExecution_worksRepeatedly() = runBlocking {\n        val bitmap = testContext.resources.getDrawable(R.drawable.ocr_card_numbers, null).toBitmap()\n        val fetcher = SSDOcrModelManager.getModelFetcher(appContext)\n        assertNotNull(fetcher)\n        assertTrue(fetcher is UpdatingResourceFetcher)\n        fetcher.clearCache()\n\n        val model = SSDOcr.Factory(appContext, fetcher.fetchData(forImmediateUse = true, isOptional = false)).newInstance()\n        assertNotNull(model)\n\n        val prediction1 = model.analyze(\n            SSDOcr.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        val prediction2 = model.analyze(\n            SSDOcr.cameraPreviewToInput(\n                TrackedImage(bitmap, Stats.trackTask(\"no_op\")),\n                bitmap.size().toRect(),\n                bitmap.size().toRect(),\n            ),\n            Unit\n        )\n        assertNotNull(prediction1)\n        // TODO: this is inconsistent due to the low quality of the minimal model, and the OCR result will change from run to run.\n        // assertEquals(\"3023334877861104\", prediction1.pan)\n\n        assertNotNull(prediction2)\n        // TODO: this is inconsistent due to the low quality of the minimal model, and the OCR result will change from run to run.\n        // assertEquals(\"3023334877861104\", prediction2.pan)\n\n        Unit\n    }\n}\n"
  },
  {
    "path": "scan-payment-minimal/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.getbouncer.scan.payment.minimal\" />\n"
  },
  {
    "path": "scan-ui/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "scan-ui/README.md",
    "content": "# Deprecation Notice\nHello from the Stripe (formerly Bouncer) team!\n\nWe're excited to provide an update on the state and future of the [Card Scan OCR](https://github.com/stripe/stripe-android/tree/master/stripecardscan) product! As we continue to build into Stripe's ecosystem, we'll be supporting the mission to continuously improve the end customer experience in many of Stripe's core checkout products.\n\nThis SDK has been [migrated to Stripe](https://github.com/stripe/stripe-android/tree/master/stripecardscan) and is now free for use under the MIT license!\n\nIf you are not currently a Stripe user, and interested in learning more about improving checkout experience through Stripe, please let us know and we can connect you with the team.\n\nIf you are not currently a Stripe user, and want to continue using the existing SDK, you can do so free of charge. Starting January 1, 2022, we will no longer be charging for use of the existing Bouncer Card Scan OCR SDK. For product support on [Android](https://github.com/stripe/stripe-android/issues) and [iOS](https://github.com/stripe/stripe-ios/issues). For billing support, please email [bouncer-support@stripe.com](mailto:bouncer-support@stripe.com).\n\nFor the new product, please visit the [stripe github repository](https://github.com/stripe/stripe-android/tree/master/stripecardscan).\n\n# Overview\nThis repository provides the legacy, deprecated open source user interfaces for scanning cards. [CardScan](https://cardscan.io/) is a relatively small library that provides fast and accurate payment card scanning.\n\nThis library is the foundation for CardScan and CardVerify enterprise libraries, which validate the authenticity of payment cards as they are scanned.\n\n![demo](../docs/images/demo.gif)\n\n## Contents\n* [Requirements](#requirements)\n* [Demo](#demo)\n* [Integration](#integration)\n* [Using](#using)\n* [Customizing](#customizing)\n* [Developing](#developing)\n* [Authors](#authors)\n* [License](#license)\n\n## Requirements\n* Android API level 21 or higher\n* AndroidX compatibility\n* Kotlin coroutine compatibility\n\nNote: Your app does not have to be written in kotlin to integrate this library, but must be able to depend on kotlin functionality.\n\n## Demo\nAn app demonstrating the basic capabilities of this library is available in [github](https://github.com/getbouncer/cardscan-demo-android).\n\n## Integration\nSee the [integration documentation](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) in the Bouncer Docs.\n\n## Using\nThis library provides a framework for scanning objects (cards, identification, etc). The abstract `ScanActivity` class provides connections to the camera and a set of common scan functionality. By extending this class, you can build your own user interface for scanning.\n\nSee the [CardScan UI](https://github.com/getbouncer/cardscan-ui-android/blob/master/cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/CardScanActivity.kt) and [Single Activity Demo](https://github.com/getbouncer/cardscan-demo-android/blob/master/demo/src/main/java/com/getbouncer/cardscan/demo/SingleActivityDemo.java) for examples.\n\n## Customizing\nThis library is built to be customized to fit your UI.\n\n### Basic modifications\nTo modify text, colors, or padding of the default UI, see the [customization](https://docs.getbouncer.com/card-scan/android-integration-guide/android-customization-guide) documentation.\n\n### Extensive modifications\nThis library is designed to be extended by a custom UI. Create an activity that extends the `ScanActivity` to build your own user interface on top of the CardScan logic.\n\n## Developing\nSee the [development docs](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) for details on developing this library.\n\n## Authors\nAdam Wushensky, Sam King, and Zain ul Abi Din\n\n## License\nThis library is available under the MIT license. See the [LICENSE](../LICENSE) file for the full license text.\n"
  },
  {
    "path": "scan-ui/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-parcelize'\n\nandroid {\n    compileSdkVersion 30\n    buildToolsVersion '30.0.3'\n\n    defaultConfig {\n        minSdkVersion 21\n        targetSdkVersion 30\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles 'consumer-rules.pro'\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    testOptions {\n        unitTests.includeAndroidResources = true\n    }\n\n    lintOptions {\n        enable \"Interoperability\"\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation project(\":scan-framework\")\n    implementation project(':scan-camera')\n\n    implementation \"androidx.appcompat:appcompat:[1.3.0,1.3.1]\"\n    implementation \"androidx.constraintlayout:constraintlayout:[2.0.4,2.1.0]\"\n    implementation \"androidx.core:core-ktx:[1.3.1,1.6.0]\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-core:[1.4.0,1.5.1]\"\n}\n\ndependencies {\n    testImplementation \"androidx.test:core:1.4.0\"\n    testImplementation \"androidx.test:runner:1.4.0\"\n    testImplementation \"junit:junit:4.13.2\"\n    testImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n}\n\ndependencies {\n    androidTestImplementation \"androidx.test.ext:junit:1.1.3\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.4.0\"\n    androidTestImplementation \"org.jetbrains.kotlin:kotlin-test:1.5.30\"\n}\n\napply from: 'deploy.gradle'\n"
  },
  {
    "path": "scan-ui/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "scan-ui/deploy.gradle",
    "content": "apply plugin: 'maven-publish'\napply plugin: 'org.jetbrains.dokka'\napply plugin: 'signing'\n\ntask androidSourcesJar(type: Jar) {\n    archiveClassifier.set('sources')\n    if (project.plugins.findPlugin(\"com.android.library\")) {\n        // Android library\n        from android.sourceSets.main.java.srcDirs\n        from android.sourceSets.main.kotlin.srcDirs\n    } else {\n        // Pure kotlin library\n        from sourceSets.main.java.srcDirs\n        from sourceSets.main.kotlin.srcDirs\n    }\n}\n\ntasks.withType(dokkaHtmlPartial.getClass()).configureEach {\n    pluginsMapConfiguration.set(\n            [\"org.jetbrains.dokka.base.DokkaBase\": \"\"\"{ \"separateInheritedMembers\": true}\"\"\"]\n    )\n}\n\ntask javadocJar(type: Jar, dependsOn: dokkaJavadoc) {\n    archiveClassifier.set('javadoc')\n    from dokkaJavadoc.outputDirectory\n}\n\nartifacts {\n    archives androidSourcesJar\n    archives javadocJar\n}\n\next[\"signing.keyId\"] = ''\next[\"signing.password\"] = ''\next[\"signing.secretKeyRingFile\"] = ''\n\next[\"ossrhUsername\"] = ''\next[\"ossrhPassword\"] = ''\next[\"sonatypeStagingProfileId\"] = ''\n\next {\n\n    libraryDescription = 'This library provides the base user interface for scanning'\n\n    siteUrl = 'https://getbouncer.com'\n\n    scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'\n    scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'\n    scmUrl = 'https://github.com/getbouncer/cardscan-android'\n\n    licenseName = 'bouncer-free-1'\n    licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'\n\n    developerId = 'getbouncer'\n    developerName = 'Bouncer Technologies'\n    developerEmail = 'bouncer-support@stripe.com'\n\n    publishGroupId = 'com.getbouncer'\n    publishArtifactId = 'scan-ui'\n    publishVersion = version\n}\n\ngroup = publishGroupId\nversion = publishVersion\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    Properties p = new Properties()\n    new FileInputStream(secretPropsFile).withCloseable { is ->\n        p.load(is)\n    }\n    p.each { name, value ->\n        ext[name] = value\n    }\n} else {\n    ext[\"signing.keyId\"] = System.getenv('SIGNING_KEY_ID')\n    ext[\"signing.password\"] = System.getenv('SIGNING_PASSWORD')\n    ext[\"signing.secretKeyRingFile\"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')\n\n    ext[\"ossrhUsername\"] = System.getenv('OSSRH_USERNAME')\n    ext[\"ossrhPassword\"] = System.getenv('OSSRH_PASSWORD')\n\n    ext[\"sonatypeStagingProfileId\"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')\n}\n\npublishing {\n    publications {\n        release(MavenPublication) {\n            groupId publishGroupId\n            artifactId publishArtifactId\n            version publishVersion\n\n            // Two artifacts, the `aar` (or `jar`) and the sources\n            if (project.plugins.findPlugin(\"com.android.library\")) {\n                artifact(\"$buildDir/outputs/aar/${project.getName()}-release.aar\")\n            } else {\n                artifact(\"$buildDir/libs/${project.getName()}-${version}.jar\")\n            }\n            artifact androidSourcesJar\n\n            pom {\n                name = publishArtifactId\n                description = libraryDescription\n                url = siteUrl\n                licenses {\n                    license {\n                        name = licenseName\n                        url = licenseUrl\n                    }\n                }\n                developers {\n                    developer {\n                        id = developerId\n                        name = developerName\n                        email = developerEmail\n                    }\n                }\n                scm {\n                    connection = scmConnection\n                    developerConnection = scmDeveloperConnection\n                    url = scmUrl\n                }\n                // A slightly hacky fix so that your POM will include any transitive dependencies\n                // that your library builds upon\n                withXml {\n                    def dependenciesNode = asNode().appendNode('dependencies')\n\n                    project.configurations.implementation.allDependencies.each {\n                        if (it.group != null && it.version != null) {\n                            def dependencyNode = dependenciesNode.appendNode('dependency')\n                            dependencyNode.appendNode('groupId', it.group)\n                            dependencyNode.appendNode('artifactId', it.name)\n                            dependencyNode.appendNode('version', it.version)\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // The repository to publish to, Sonatype/MavenCentral\n    repositories {\n        maven {\n            // This is an arbitrary name, you may also use \"mavencentral\" or\n            // any other name that's descriptive for you\n            name = \"sonatype\"\n            url = \"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/\"\n            credentials {\n                username ossrhUsername\n                password ossrhPassword\n            }\n        }\n    }\n}\n\nsigning {\n    sign publishing.publications\n}\n"
  },
  {
    "path": "scan-ui/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": "scan-ui/src/androidTest/java/com/getbouncer/scan/ui/DebugOverlayTest.kt",
    "content": "package com.getbouncer.scan.ui\n\nimport android.graphics.RectF\nimport android.util.Size\nimport androidx.test.filters.SmallTest\nimport org.junit.Test\nimport kotlin.test.assertEquals\n\nclass DebugOverlayTest {\n\n    @Test\n    @SmallTest\n    fun scaleRect() {\n        assertEquals(RectF(2F, 4F, 6F, 8F), RectF(0.05F, 0.10F, 0.15F, 0.20F).scaled(Size(40, 40)))\n    }\n}\n"
  },
  {
    "path": "scan-ui/src/androidTest/java/com/getbouncer/scan/ui/util/ViewExtensionsTest.kt",
    "content": "package com.getbouncer.scan.ui.util\n\nimport androidx.test.filters.SmallTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.getbouncer.scan.ui.test.R\nimport org.junit.Test\nimport kotlin.test.assertEquals\n\nclass ViewExtensionsTest {\n    private val testContext = InstrumentationRegistry.getInstrumentation().context\n\n    @Test\n    @SmallTest\n    fun getColorByRes_matches() {\n        val color = testContext.getColorByRes(R.color.testColor)\n        val alpha = color shr 24 and 0xFF\n        val red = color shr 16 and 0xFF\n        val green = color shr 8 and 0xFF\n        val blue = color and 0xFF\n\n        assertEquals(0xA1, alpha)\n        assertEquals(0x1E, red)\n        assertEquals(0x90, green)\n        assertEquals(0xFF, blue)\n    }\n}\n"
  },
  {
    "path": "scan-ui/src/androidTest/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"testColor\">#A11E90FF</color>\n</resources>\n"
  },
  {
    "path": "scan-ui/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.getbouncer.scan.ui\" />\n"
  },
  {
    "path": "scan-ui/src/main/java/com/getbouncer/scan/ui/DebugOverlay.kt",
    "content": "package com.getbouncer.scan.ui\n\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.Paint\nimport android.graphics.RectF\nimport android.util.AttributeSet\nimport android.util.Size\nimport android.util.TypedValue\nimport android.view.View\nimport androidx.annotation.VisibleForTesting\n\n@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ninternal fun RectF.scaled(scaledSize: Size): RectF {\n    return RectF(\n        this.left * scaledSize.width,\n        this.top * scaledSize.height,\n        this.right * scaledSize.width,\n        this.bottom * scaledSize.height\n    )\n}\n\n/**\n * A detection box to display on the debug overlay.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ndata class DebugDetectionBox(\n    val rect: RectF,\n\n    val confidence: Float,\n\n    val label: String\n)\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nclass DebugOverlay(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {\n\n    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {\n        style = Paint.Style.STROKE\n        strokeWidth = 2F\n    }\n\n    private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {\n        style = Paint.Style.FILL\n        textSize = TypedValue.applyDimension(\n            TypedValue.COMPLEX_UNIT_SP,\n            20F,\n            resources.displayMetrics\n        )\n        textAlign = Paint.Align.LEFT\n    }\n\n    private var boxes: Collection<DebugDetectionBox>? = null\n\n    override fun onDraw(canvas: Canvas?) {\n        super.onDraw(canvas)\n        if (canvas != null) drawBoxes(canvas)\n    }\n\n    private fun drawBoxes(canvas: Canvas) {\n        // This should be using boxes?.forEach, but doing so seems to require API 24. It's unclear why this won't\n        // use the kotlin.collections version of `forEach`, but it's not during compile.\n        for (it in boxes ?: emptyList()) {\n            paint.color = getPaintColor(it.confidence)\n            textPaint.color = getPaintColor(it.confidence)\n            val rect = it.rect.scaled(Size(this.width, this.height))\n            canvas.drawRect(rect, paint)\n            canvas.drawText(it.label, rect.left, rect.bottom, textPaint)\n        }\n    }\n\n    @Suppress(\"Deprecation\")\n    private fun getPaintColor(confidence: Float) = context.resources.getColor(\n        when {\n            confidence > 0.75 -> R.color.bouncerDebugHighConfidence\n            confidence > 0.5 -> R.color.bouncerDebugMediumConfidence\n            else -> R.color.bouncerDebugLowConfidence\n        }\n    )\n\n    fun setBoxes(boxes: Collection<DebugDetectionBox>?) {\n        this.boxes = boxes\n        invalidate()\n        requestLayout()\n    }\n\n    fun clearBoxes() {\n        setBoxes(emptyList())\n    }\n}\n"
  },
  {
    "path": "scan-ui/src/main/java/com/getbouncer/scan/ui/ScanActivity.kt",
    "content": "package com.getbouncer.scan.ui\n\nimport android.Manifest\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.graphics.Bitmap\nimport android.graphics.PointF\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.provider.Settings\nimport android.util.Log\nimport android.util.Size\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.WindowManager\nimport androidx.annotation.StringRes\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.app.ActivityCompat\nimport androidx.core.content.ContextCompat\nimport com.getbouncer.scan.camera.CameraAdapter\nimport com.getbouncer.scan.camera.CameraErrorListener\nimport com.getbouncer.scan.camera.CameraPreviewImage\nimport com.getbouncer.scan.camera.getCameraAdapter\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.Stats\nimport com.getbouncer.scan.framework.StorageFactory\nimport com.getbouncer.scan.framework.api.ERROR_CODE_NOT_AUTHENTICATED\nimport com.getbouncer.scan.framework.api.NetworkResult\nimport com.getbouncer.scan.framework.api.dto.ScanStatistics\nimport com.getbouncer.scan.framework.api.uploadScanStats\nimport com.getbouncer.scan.framework.api.validateApiKey\nimport com.getbouncer.scan.framework.util.AppDetails\nimport com.getbouncer.scan.framework.util.Device\nimport com.getbouncer.scan.framework.util.getAppPackageName\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.parcelize.Parcelize\nimport kotlin.coroutines.CoroutineContext\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nconst val PERMISSION_RATIONALE_SHOWN = \"permission_rationale_shown\"\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ninterface ScanResultListener {\n\n    /**\n     * The user canceled the scan.\n     */\n    fun userCanceled(reason: CancellationReason)\n\n    /**\n     * The scan failed because of an error.\n     */\n    fun failed(cause: Throwable?)\n}\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nsealed interface CancellationReason : Parcelable {\n\n    @Parcelize\n    object Closed : CancellationReason\n\n    @Parcelize\n    object Back : CancellationReason\n\n    @Parcelize\n    object UserCannotScan : CancellationReason\n\n    @Parcelize\n    object CameraPermissionDenied : CancellationReason\n}\n\n/**\n * A basic implementation that displays error messages when there is a problem with the camera.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nopen class CameraErrorListenerImpl(\n    protected val context: Context,\n    protected val callback: (Throwable?) -> Unit\n) : CameraErrorListener {\n    override fun onCameraOpenError(cause: Throwable?) {\n        showCameraError(R.string.bouncer_error_camera_open, cause)\n    }\n\n    override fun onCameraAccessError(cause: Throwable?) {\n        showCameraError(R.string.bouncer_error_camera_access, cause)\n    }\n\n    override fun onCameraUnsupportedError(cause: Throwable?) {\n        Log.e(Config.logTag, \"Camera not supported\", cause)\n        showCameraError(R.string.bouncer_error_camera_unsupported, cause)\n    }\n\n    private fun showCameraError(@StringRes message: Int, cause: Throwable?) {\n        AlertDialog.Builder(context)\n            .setTitle(R.string.bouncer_error_camera_title)\n            .setMessage(message)\n            .setPositiveButton(R.string.bouncer_error_camera_acknowledge_button) { _, _ -> callback(cause) }\n            .show()\n    }\n}\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nabstract class ScanActivity : AppCompatActivity(), CoroutineScope {\n    companion object {\n        const val PERMISSION_REQUEST_CODE = 1200\n    }\n\n    override val coroutineContext: CoroutineContext = Dispatchers.Main\n\n    protected val scanStat = Stats.trackTask(\"scan_activity\")\n    private val permissionStat = Stats.trackTask(\"camera_permission\")\n\n    protected var isFlashlightOn: Boolean = false\n        private set\n\n    protected val cameraAdapter by lazy { buildCameraAdapter() }\n    private val cameraErrorListener by lazy {\n        CameraErrorListenerImpl(this) { t -> cameraErrorCancelScan(t) }\n    }\n\n    /**\n     * The listener which will handle the results from the scan.\n     */\n    protected abstract val resultListener: ScanResultListener\n\n    protected val storage by lazy {\n        StorageFactory.getStorageInstance(this, \"scan_camera_permissions\")\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        Stats.startScan()\n\n        ensureValidApiKey()\n\n        if (!CameraAdapter.isCameraSupported(this)) {\n            showCameraNotSupportedDialog()\n        }\n    }\n\n    override fun onResume() {\n        super.onResume()\n        hideSystemUi()\n\n        launch {\n            delay(1500)\n            hideSystemUi()\n        }\n\n        if (!cameraAdapter.isBoundToLifecycle()) {\n            ensurePermissionAndStartCamera()\n        }\n    }\n\n    protected open fun hideSystemUi() {\n        // Prevent screenshots and keep the screen on while scanning.\n        window.setFlags(\n            WindowManager.LayoutParams.FLAG_SECURE + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,\n            WindowManager.LayoutParams.FLAG_SECURE + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON\n        )\n\n        // Hide both the navigation bar and the status bar. Allow system gestures to show the navigation and status bar,\n        // but prevent the UI from resizing when they are shown.\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n            window.setDecorFitsSystemWindows(true)\n        } else {\n            @Suppress(\"deprecation\")\n            window.decorView.apply {\n                systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or\n                    View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or\n                    View.SYSTEM_UI_FLAG_FULLSCREEN or\n                    View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or\n                    View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or\n                    View.SYSTEM_UI_FLAG_LAYOUT_STABLE\n            }\n        }\n    }\n\n    override fun onPause() {\n        super.onPause()\n        setFlashlightState(false)\n    }\n\n    /**\n     * Ensure that the camera permission is available. If so, start the camera. If not, request it.\n     */\n    protected open fun ensurePermissionAndStartCamera() = when {\n        ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {\n            runBlocking { permissionStat.trackResult(\"already_granted\") }\n            prepareCamera { onCameraReady() }\n        }\n        ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA) -> showPermissionRationaleDialog()\n        storage.getBoolean(PERMISSION_RATIONALE_SHOWN, false) -> showPermissionDeniedDialog()\n        else -> requestCameraPermission()\n    }\n\n    /**\n     * Handle permission status changes. If the camera permission has been granted, start it. If\n     * not, show a dialog.\n     */\n    override fun onRequestPermissionsResult(\n        requestCode: Int,\n        permissions: Array<out String>,\n        grantResults: IntArray\n    ) {\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults)\n\n        if (requestCode == PERMISSION_REQUEST_CODE && grantResults.isNotEmpty()) {\n            when (grantResults[0]) {\n                PackageManager.PERMISSION_GRANTED -> {\n                    runBlocking { permissionStat.trackResult(\"granted\") }\n                    prepareCamera { onCameraReady() }\n                }\n                else -> {\n                    runBlocking { permissionStat.trackResult(\"denied\") }\n                    cameraPermissionDenied()\n                }\n            }\n        }\n    }\n\n    /**\n     * Show a dialog explaining that the camera is not available.\n     */\n    protected open fun showCameraNotSupportedDialog() {\n        AlertDialog.Builder(this)\n            .setTitle(R.string.bouncer_error_camera_title)\n            .setMessage(R.string.bouncer_error_camera_unsupported)\n            .setPositiveButton(R.string.bouncer_error_camera_acknowledge_button) { _, _ -> cameraErrorCancelScan() }\n            .show()\n    }\n\n    /**\n     * Show an explanation dialog for why we are requesting camera permissions.\n     */\n    protected open fun showPermissionRationaleDialog() {\n        val builder = AlertDialog.Builder(this)\n        builder.setMessage(R.string.bouncer_camera_permission_denied_message)\n            .setPositiveButton(R.string.bouncer_camera_permission_denied_ok) { _, _ -> requestCameraPermission() }\n        builder.show()\n        storage.storeValue(PERMISSION_RATIONALE_SHOWN, true)\n    }\n\n    /**\n     * Show an explanation dialog for why we are requesting camera permissions when the permission\n     * has been permanently denied.\n     */\n    protected open fun showPermissionDeniedDialog() {\n        val builder = AlertDialog.Builder(this)\n        builder.setMessage(R.string.bouncer_camera_permission_denied_message)\n            .setPositiveButton(R.string.bouncer_camera_permission_denied_ok) { _, _ ->\n                storage.storeValue(PERMISSION_RATIONALE_SHOWN, false)\n                openAppSettings()\n            }\n            .setNegativeButton(R.string.bouncer_camera_permission_denied_cancel) { _, _ -> cameraPermissionDenied() }\n        builder.show()\n    }\n\n    /**\n     * Request permission to use the camera.\n     */\n    protected open fun requestCameraPermission() {\n        ActivityCompat.requestPermissions(\n            this,\n            arrayOf(Manifest.permission.CAMERA),\n            PERMISSION_REQUEST_CODE,\n        )\n    }\n\n    /**\n     * Open the settings for this app\n     */\n    protected open fun openAppSettings() {\n        val intent = Intent()\n            .setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)\n            .setData(Uri.fromParts(\"package\", getAppPackageName(this), null))\n        startActivity(intent)\n    }\n\n    /**\n     * Validate the API key against the server. If it's invalid, close the scanner.\n     */\n    protected fun ensureValidApiKey() {\n        if (Config.apiKey != null) {\n            launch {\n                when (val apiKeyValidateResult = validateApiKey(this@ScanActivity)) {\n                    is NetworkResult.Success -> {\n                        if (!apiKeyValidateResult.body.isApiKeyValid) {\n                            Log.e(\n                                Config.logTag,\n                                \"API key is invalid: ${apiKeyValidateResult.body.keyInvalidReason}\"\n                            )\n                            onInvalidApiKey()\n                            showApiKeyInvalidError()\n                        }\n                    }\n                    is NetworkResult.Error -> {\n                        if (apiKeyValidateResult.error.errorCode == ERROR_CODE_NOT_AUTHENTICATED) {\n                            Log.e(\n                                Config.logTag,\n                                \"API key is invalid: ${apiKeyValidateResult.error.errorMessage}\"\n                            )\n                            onInvalidApiKey()\n                            showApiKeyInvalidError()\n                        } else {\n                            Log.w(\n                                Config.logTag,\n                                \"Unable to validate API key: ${apiKeyValidateResult.error.errorMessage}\"\n                            )\n                        }\n                    }\n                    is NetworkResult.Exception -> Log.w(\n                        Config.logTag,\n                        \"Unable to validate API key\",\n                        apiKeyValidateResult.exception\n                    )\n                }\n            }\n        }\n    }\n\n    protected open fun showApiKeyInvalidError() {\n        AlertDialog.Builder(this)\n            .setTitle(R.string.bouncer_api_key_invalid_title)\n            .setMessage(R.string.bouncer_api_key_invalid_message)\n            .setPositiveButton(R.string.bouncer_api_key_invalid_ok) { _, _ -> userCancelScan() }\n            .setCancelable(false)\n            .show()\n    }\n\n    /**\n     * Turn the flashlight on or off.\n     */\n    protected open fun toggleFlashlight() {\n        isFlashlightOn = !isFlashlightOn\n        setFlashlightState(isFlashlightOn)\n        runBlocking { Stats.trackRepeatingTask(\"torch_state\").trackResult(if (isFlashlightOn) \"on\" else \"off\") }\n    }\n\n    /**\n     * Toggle between available cameras.\n     */\n    protected open fun toggleCamera() {\n        cameraAdapter.changeCamera()\n        runBlocking { Stats.trackRepeatingTask(\"swap_camera\").trackResult(\"${cameraAdapter.getCurrentCamera()}\") }\n    }\n\n    /**\n     * Called when the flashlight state has changed.\n     */\n    protected abstract fun onFlashlightStateChanged(flashlightOn: Boolean)\n\n    /**\n     * Turn the flashlight on or off.\n     */\n    private fun setFlashlightState(on: Boolean) {\n        cameraAdapter.setTorchState(on)\n        isFlashlightOn = on\n        onFlashlightStateChanged(on)\n    }\n\n    /**\n     * Cancel scanning due to a camera error.\n     */\n    protected open fun cameraErrorCancelScan(cause: Throwable? = null) {\n        Log.e(Config.logTag, \"Canceling scan due to camera error\", cause)\n        runBlocking { scanStat.trackResult(\"camera_error\") }\n        resultListener.failed(cause)\n        closeScanner()\n    }\n\n    /**\n     * The scan has been cancelled by the user.\n     */\n    protected open fun userCancelScan() {\n        runBlocking { scanStat.trackResult(\"user_canceled\") }\n        resultListener.userCanceled(CancellationReason.Closed)\n        closeScanner()\n    }\n\n    protected open fun cameraPermissionDenied() {\n        runBlocking { scanStat.trackResult(\"user_canceled\") }\n        resultListener.userCanceled(CancellationReason.CameraPermissionDenied)\n        closeScanner()\n    }\n\n    /**\n     * Cancel scanning due to analyzer failure\n     */\n    protected open fun analyzerFailureCancelScan(cause: Throwable? = null) {\n        Log.e(Config.logTag, \"Canceling scan due to analyzer error\", cause)\n        runBlocking { scanStat.trackResult(\"analyzer_failure\") }\n        resultListener.failed(cause)\n        closeScanner()\n    }\n\n    /**\n     * Close the scanner.\n     */\n    protected open fun closeScanner() {\n        setFlashlightState(false)\n        if (Config.uploadStats) {\n            uploadStats(\n                instanceId = Stats.instanceId,\n                scanId = Stats.scanId,\n                device = Device.fromContext(this),\n                appDetails = AppDetails.fromContext(this),\n                scanStatistics = ScanStatistics.fromStats(),\n            )\n        }\n        finish()\n    }\n\n    /**\n     * Upload stats to the bouncer servers. Override this to perform some other action.\n     */\n    protected open fun uploadStats(\n        instanceId: String,\n        scanId: String?,\n        device: Device,\n        appDetails: AppDetails,\n        scanStatistics: ScanStatistics\n    ) {\n        uploadScanStats(\n            context = this,\n            instanceId = instanceId,\n            scanId = scanId,\n            device = device,\n            appDetails = appDetails,\n            scanStatistics = scanStatistics\n        )\n    }\n\n    /**\n     * Prepare to start the camera. Once the camera is ready, [onCameraReady] must be called.\n     */\n    protected abstract fun prepareCamera(onCameraReady: () -> Unit)\n\n    protected open fun onCameraReady() {\n        cameraAdapter.bindToLifecycle(this)\n\n        val torchStat = Stats.trackTask(\"torch_supported\")\n        cameraAdapter.withFlashSupport {\n            runBlocking { torchStat.trackResult(if (it) \"supported\" else \"unsupported\") }\n            setFlashlightState(cameraAdapter.isTorchOn())\n            onFlashSupported(it)\n        }\n\n        val cameraStat = Stats.trackTask(\"multiple_cameras_supported\")\n        cameraAdapter.withSupportsMultipleCameras {\n            runBlocking { cameraStat.trackResult(if (it) \"supported\" else \"unsupported\") }\n            onSupportsMultipleCameras(it)\n        }\n\n        onCameraStreamAvailable(cameraAdapter.getImageStream())\n    }\n\n    /**\n     * Perform an action when the flash is supported\n     */\n    protected abstract fun onFlashSupported(supported: Boolean)\n\n    /**\n     * Perform an action when the camera support is determined\n     */\n    protected abstract fun onSupportsMultipleCameras(supported: Boolean)\n\n    protected open fun setFocus(point: PointF) {\n        cameraAdapter.setFocus(point)\n    }\n\n    /**\n     * Cancel the scan when the user presses back.\n     */\n    override fun onBackPressed() {\n        runBlocking { scanStat.trackResult(\"user_canceled\") }\n        resultListener.userCanceled(CancellationReason.Back)\n        closeScanner()\n    }\n\n    /**\n     * Generate a camera adapter\n     */\n    protected open fun buildCameraAdapter(): CameraAdapter<CameraPreviewImage<Bitmap>> =\n        getCameraAdapter(\n            activity = this,\n            previewView = previewFrame,\n            minimumResolution = minimumAnalysisResolution,\n            cameraErrorListener = cameraErrorListener,\n        )\n\n    protected abstract val previewFrame: ViewGroup\n\n    protected abstract val minimumAnalysisResolution: Size\n\n    /**\n     * A stream of images from the camera is available to be processed.\n     */\n    protected abstract fun onCameraStreamAvailable(cameraStream: Flow<CameraPreviewImage<Bitmap>>)\n\n    /**\n     * The API key was invalid.\n     */\n    protected abstract fun onInvalidApiKey()\n}\n"
  },
  {
    "path": "scan-ui/src/main/java/com/getbouncer/scan/ui/ScanFlow.kt",
    "content": "package com.getbouncer.scan.ui\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.Rect\nimport androidx.lifecycle.LifecycleOwner\nimport com.getbouncer.scan.camera.CameraPreviewImage\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.Flow\n\n/**\n * A flow for scanning something. This manages the callbacks and lifecycle of the flow.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\ninterface ScanFlow {\n\n    /**\n     * Start the image processing flow for scanning a card.\n     *\n     * @param context: The context used to download analyzers if needed\n     * @param imageStream: The flow of images to process\n     * @param viewFinder: The location of the view finder in the previewSize\n     * @param lifecycleOwner: The activity that owns this flow. The flow will pause if the activity\n     * is paused\n     * @param coroutineScope: The coroutine scope used to run async tasks for this flow\n     */\n    fun startFlow(\n        context: Context,\n        imageStream: Flow<CameraPreviewImage<Bitmap>>,\n        viewFinder: Rect,\n        lifecycleOwner: LifecycleOwner,\n        coroutineScope: CoroutineScope\n    )\n\n    /**\n     * In the event that the scan cannot complete, halt the flow to halt analyzers and free up CPU and memory.\n     */\n    fun cancelFlow()\n}\n"
  },
  {
    "path": "scan-ui/src/main/java/com/getbouncer/scan/ui/SimpleScanActivity.kt",
    "content": "package com.getbouncer.scan.ui\n\nimport android.annotation.SuppressLint\nimport android.content.res.Resources\nimport android.graphics.Bitmap\nimport android.graphics.PointF\nimport android.graphics.Typeface\nimport android.os.Bundle\nimport android.util.Size\nimport android.view.Gravity\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.FrameLayout\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.constraintlayout.widget.ConstraintLayout\nimport androidx.constraintlayout.widget.ConstraintSet\nimport androidx.core.content.ContextCompat\nimport com.getbouncer.scan.camera.CameraPreviewImage\nimport com.getbouncer.scan.framework.Config\nimport com.getbouncer.scan.framework.util.getSdkVersion\nimport com.getbouncer.scan.ui.util.asRect\nimport com.getbouncer.scan.ui.util.dpToPixels\nimport com.getbouncer.scan.ui.util.getColorByRes\nimport com.getbouncer.scan.ui.util.getDrawableByRes\nimport com.getbouncer.scan.ui.util.getFloatResource\nimport com.getbouncer.scan.ui.util.hide\nimport com.getbouncer.scan.ui.util.setDrawable\nimport com.getbouncer.scan.ui.util.setTextSizeByRes\nimport com.getbouncer.scan.ui.util.setVisible\nimport com.getbouncer.scan.ui.util.show\nimport com.getbouncer.scan.ui.util.startAnimation\nimport kotlinx.coroutines.flow.Flow\nimport kotlin.math.min\nimport kotlin.math.roundToInt\n\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nabstract class SimpleScanActivity : ScanActivity() {\n\n    /**\n     * The state of the scan flow. This can be expanded if [displayState] is overridden to handle\n     * the added states.\n     */\n    @Deprecated(\n        message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n        replaceWith = ReplaceWith(\"StripeCardScan\"),\n    )\n    abstract class ScanState(val isFinal: Boolean) {\n        object NotFound : ScanState(isFinal = false)\n        object FoundShort : ScanState(isFinal = false)\n        object FoundLong : ScanState(isFinal = false)\n        object Correct : ScanState(isFinal = true)\n        object Wrong : ScanState(isFinal = false)\n    }\n\n    companion object {\n        private const val LOGO_WIDTH_DP = 100\n    }\n\n    /**\n     * The main layout used to render the scan view.\n     */\n    protected open val layout: ConstraintLayout by lazy { ConstraintLayout(this) }\n\n    /**\n     * The frame where the camera preview will be displayed. This is usually the full screen.\n     */\n    override val previewFrame: ViewGroup by lazy { FrameLayout(this) }\n\n    /**\n     * The text view that displays the cardholder name once a card has been scanned.\n     */\n    protected open val cardNameTextView: TextView by lazy { TextView(this) }\n\n    /**\n     * The text view that displays the card number once a card has been scanned.\n     */\n    protected open val cardNumberTextView: TextView by lazy { TextView(this) }\n\n    /**\n     * The view that the user can tap to close the scan window.\n     */\n    protected open val closeButtonView: View by lazy { ImageView(this) }\n\n    /**\n     * The view that a user can tap to turn on the flashlight.\n     */\n    protected open val torchButtonView: View by lazy { ImageView(this) }\n\n    /**\n     * The view that a user can tap to swap cameras.\n     */\n    protected open val swapCameraButtonView: View by lazy { ImageView(this) }\n\n    /**\n     * The text view that informs the user what to do.\n     */\n    protected open val instructionsTextView: TextView by lazy { TextView(this) }\n\n    /**\n     * The icon used to display a lock to indicate that the scanned card is secure.\n     */\n    protected open val securityIconView: ImageView by lazy { ImageView(this) }\n\n    /**\n     * The text view used to inform the user that the scanned card is secure.\n     */\n    protected open val securityTextView: TextView by lazy { TextView(this) }\n\n    /**\n     * The background that draws the user focus to the view finder.\n     */\n    protected open val viewFinderBackgroundView: ViewFinderBackground by lazy { ViewFinderBackground(this) }\n\n    /**\n     * The view finder window view.\n     */\n    protected open val viewFinderWindowView: View by lazy { View(this) }\n\n    /**\n     * The border around the view finder.\n     */\n    protected open val viewFinderBorderView: ImageView by lazy { ImageView(this) }\n\n    /**\n     * The image view that shows the currently processing frame\n     */\n    protected open val debugImageView: ImageView by lazy { ImageView(this) }\n\n    /**\n     * The overlay that shows details about the currently processing frame.\n     */\n    protected open val debugOverlayView: DebugOverlay by lazy { DebugOverlay(this) }\n\n    private val logoView: ImageView by lazy { ImageView(this) }\n\n    protected open val versionTextView: TextView by lazy { TextView(this) }\n\n    /**\n     * The aspect ratio of the view finder.\n     */\n    protected open val viewFinderAspectRatio = \"200:126\"\n\n    /**\n     * Determine if the flashlight is supported.\n     */\n    protected var isFlashlightSupported: Boolean? = null\n\n    /**\n     * Determine if multiple cameras are available.\n     */\n    protected var hasMultipleCameras: Boolean? = null\n\n    /**\n     * The flow used to scan an item.\n     */\n    protected abstract val scanFlow: ScanFlow\n\n    /**\n     * Determine if the background is dark. This is used to set light background vs dark background\n     * text and images.\n     */\n    protected open fun isBackgroundDark(): Boolean =\n        viewFinderBackgroundView.getBackgroundLuminance() < 128\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        addUiComponents()\n        setupUiComponents()\n        setupUiConstraints()\n\n        setupLogoUi()\n        setupLogoConstraints()\n\n        setupVersionUi()\n        setupVersionConstraints()\n\n        closeButtonView.setOnClickListener { userCancelScan() }\n        torchButtonView.setOnClickListener { toggleFlashlight() }\n        swapCameraButtonView.setOnClickListener { toggleCamera() }\n\n        viewFinderBorderView.setOnTouchListener { _, e ->\n            setFocus(PointF(e.x + viewFinderWindowView.left, e.y + viewFinderWindowView.top))\n            true\n        }\n\n        displayState(scanState, scanStatePrevious)\n        setContentView(layout)\n    }\n\n    override fun onPause() {\n        viewFinderBackgroundView.clearOnDrawListener()\n        super.onPause()\n    }\n\n    override fun onResume() {\n        super.onResume()\n        scanState = ScanState.NotFound\n        viewFinderBackgroundView.setOnDrawListener { setupUiComponents() }\n    }\n\n    override fun onDestroy() {\n        scanFlow.cancelFlow()\n        super.onDestroy()\n    }\n\n    /**\n     * Add the UI components to the root view.\n     */\n    protected open fun addUiComponents() {\n        layout.id = View.generateViewId()\n\n        appendUiComponents(\n            previewFrame,\n            viewFinderBackgroundView,\n            viewFinderWindowView,\n            viewFinderBorderView,\n            securityIconView,\n            securityTextView,\n            instructionsTextView,\n            closeButtonView,\n            torchButtonView,\n            swapCameraButtonView,\n            cardNameTextView,\n            cardNumberTextView,\n            debugImageView,\n            debugOverlayView,\n            logoView,\n            versionTextView,\n        )\n    }\n\n    /**\n     * Append additional UI elements to the view.\n     */\n    protected fun appendUiComponents(vararg components: View) {\n        components.forEach {\n            it.id = View.generateViewId()\n            layout.addView(it)\n        }\n    }\n\n    protected open fun setupUiComponents() {\n        setupCloseButtonViewUi()\n        setupTorchButtonViewUi()\n        setupSwapCameraButtonViewUi()\n        setupViewFinderViewUI()\n        setupInstructionsViewUi()\n        setupSecurityNoticeUi()\n        setupCardDetailsUi()\n        setupDebugUi()\n    }\n\n    protected open fun setupCloseButtonViewUi() {\n        when (val view = closeButtonView) {\n            is ImageView -> {\n                view.contentDescription = getString(R.string.bouncer_close_button_description)\n                if (isBackgroundDark()) {\n                    view.setDrawable(R.drawable.bouncer_close_button_dark)\n                } else {\n                    view.setDrawable(R.drawable.bouncer_close_button_light)\n                }\n            }\n            is TextView -> {\n                view.text = getString(R.string.bouncer_close_button_description)\n                if (isBackgroundDark()) {\n                    view.setTextColor(getColorByRes(R.color.bouncerCloseButtonDarkColor))\n                } else {\n                    view.setTextColor(getColorByRes(R.color.bouncerCloseButtonLightColor))\n                }\n            }\n        }\n    }\n\n    protected open fun setupTorchButtonViewUi() {\n        torchButtonView.setVisible(isFlashlightSupported == true)\n        when (val view = torchButtonView) {\n            is ImageView -> {\n                view.contentDescription = getString(R.string.bouncer_torch_button_description)\n                if (isBackgroundDark()) {\n                    if (isFlashlightOn) {\n                        view.setDrawable(R.drawable.bouncer_flash_on_dark)\n                    } else {\n                        view.setDrawable(R.drawable.bouncer_flash_off_dark)\n                    }\n                } else {\n                    if (isFlashlightOn) {\n                        view.setDrawable(R.drawable.bouncer_flash_on_light)\n                    } else {\n                        view.setDrawable(R.drawable.bouncer_flash_off_light)\n                    }\n                }\n            }\n            is TextView -> {\n                view.text = getString(R.string.bouncer_torch_button_description)\n                if (isBackgroundDark()) {\n                    view.setTextColor(getColorByRes(R.color.bouncerFlashButtonDarkColor))\n                } else {\n                    view.setTextColor(getColorByRes(R.color.bouncerFlashButtonLightColor))\n                }\n            }\n        }\n    }\n\n    protected open fun setupSwapCameraButtonViewUi() {\n        swapCameraButtonView.setVisible(hasMultipleCameras == true)\n        when (val view = swapCameraButtonView) {\n            is ImageView -> {\n                view.contentDescription = getString(R.string.bouncer_swap_camera_button_description)\n                if (isBackgroundDark()) {\n                    view.setDrawable(R.drawable.bouncer_camera_swap_dark)\n                } else {\n                    view.setDrawable(R.drawable.bouncer_camera_swap_light)\n                }\n            }\n            is TextView -> {\n                view.text = getString(R.string.bouncer_swap_camera_button_description)\n                if (isBackgroundDark()) {\n                    view.setTextColor(getColorByRes(R.color.bouncerCameraSwapButtonDarkColor))\n                } else {\n                    view.setTextColor(getColorByRes(R.color.bouncerCameraSwapButtonLightColor))\n                }\n            }\n        }\n    }\n\n    protected open fun setupViewFinderViewUI() {\n        viewFinderBorderView.background = getDrawableByRes(R.drawable.bouncer_card_border_not_found)\n    }\n\n    protected open fun setupInstructionsViewUi() {\n        instructionsTextView.setTextSizeByRes(R.dimen.bouncerInstructionsTextSize)\n        instructionsTextView.typeface = Typeface.DEFAULT_BOLD\n        instructionsTextView.gravity = Gravity.CENTER\n\n        if (isBackgroundDark()) {\n            instructionsTextView.setTextColor(getColorByRes(R.color.bouncerInstructionsColorDark))\n        } else {\n            instructionsTextView.setTextColor(getColorByRes(R.color.bouncerInstructionsColorLight))\n        }\n    }\n\n    protected open fun setupSecurityNoticeUi() {\n        securityTextView.text = getString(R.string.bouncer_card_scan_security)\n        securityTextView.setTextSizeByRes(R.dimen.bouncerSecurityTextSize)\n        securityIconView.contentDescription = getString(R.string.bouncer_security_description)\n\n        if (isBackgroundDark()) {\n            securityTextView.setTextColor(getColorByRes(R.color.bouncerSecurityColorDark))\n            securityIconView.setDrawable(R.drawable.bouncer_lock_dark)\n        } else {\n            securityTextView.setTextColor(getColorByRes(R.color.bouncerSecurityColorLight))\n            securityIconView.setDrawable(R.drawable.bouncer_lock_light)\n        }\n    }\n\n    protected open fun setupCardDetailsUi() {\n        cardNumberTextView.setTextColor(getColorByRes(R.color.bouncerCardPanColor))\n        cardNumberTextView.setTextSizeByRes(R.dimen.bouncerPanTextSize)\n        cardNumberTextView.gravity = Gravity.CENTER\n        cardNumberTextView.typeface = Typeface.DEFAULT_BOLD\n        cardNumberTextView.setShadowLayer(getFloatResource(R.dimen.bouncerPanStrokeSize), 0F, 0F, getColorByRes(R.color.bouncerCardPanOutlineColor))\n\n        cardNameTextView.setTextColor(getColorByRes(R.color.bouncerCardNameColor))\n        cardNameTextView.setTextSizeByRes(R.dimen.bouncerNameTextSize)\n        cardNameTextView.gravity = Gravity.CENTER\n        cardNameTextView.typeface = Typeface.DEFAULT_BOLD\n        cardNameTextView.setShadowLayer(getFloatResource(R.dimen.bouncerNameStrokeSize), 0F, 0F, getColorByRes(R.color.bouncerCardNameOutlineColor))\n    }\n\n    protected open fun setupDebugUi() {\n        debugImageView.contentDescription = getString(R.string.bouncer_debug_description)\n        debugImageView.setVisible(Config.isDebug)\n        debugOverlayView.setVisible(Config.isDebug)\n    }\n\n    private fun setupLogoUi() {\n        if (isBackgroundDark()) {\n            logoView.setImageDrawable(\n                ContextCompat.getDrawable(this, R.drawable.bouncer_logo_dark_background)\n            )\n        } else {\n            logoView.setImageDrawable(\n                ContextCompat.getDrawable(this, R.drawable.bouncer_logo_light_background)\n            )\n        }\n\n        logoView.contentDescription = getString(R.string.bouncer_cardscan_logo)\n        logoView.setVisible(Config.displayLogo)\n    }\n\n    private fun setupVersionUi() {\n        versionTextView.text = getSdkVersion()\n        versionTextView.setTextSizeByRes(R.dimen.bouncerSecurityTextSize)\n        versionTextView.setVisible(Config.isDebug)\n\n        if (isBackgroundDark()) {\n            versionTextView.setTextColor(getColorByRes(R.color.bouncerSecurityColorDark))\n        }\n    }\n\n    protected open fun setupUiConstraints() {\n        setupPreviewFrameConstraints()\n        setupCloseButtonViewConstraints()\n        setupTorchButtonViewConstraints()\n        setupSwapCameraButtonViewConstraints()\n        setupViewFinderConstraints()\n        setupInstructionsViewConstraints()\n        setupSecurityNoticeConstraints()\n        setupCardDetailsConstraints()\n        setupDebugConstraints()\n    }\n\n    protected open fun setupPreviewFrameConstraints() {\n        previewFrame.layoutParams = ConstraintLayout.LayoutParams(0, 0)\n        previewFrame.constrainToParent()\n    }\n\n    protected open fun setupCloseButtonViewConstraints() {\n        closeButtonView.layoutParams = ConstraintLayout.LayoutParams(\n            ViewGroup.LayoutParams.WRAP_CONTENT, // width\n            ViewGroup.LayoutParams.WRAP_CONTENT, // height\n        ).apply {\n            topMargin = resources.getDimensionPixelSize(R.dimen.bouncerButtonMargin)\n            bottomMargin = resources.getDimensionPixelSize(R.dimen.bouncerButtonMargin)\n            marginStart = resources.getDimensionPixelSize(R.dimen.bouncerButtonMargin)\n            marginEnd = resources.getDimensionPixelSize(R.dimen.bouncerButtonMargin)\n        }\n\n        closeButtonView.addConstraints {\n            connect(it.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)\n            connect(it.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)\n        }\n    }\n\n    protected open fun setupTorchButtonViewConstraints() {\n        torchButtonView.layoutParams = ConstraintLayout.LayoutParams(\n            ViewGroup.LayoutParams.WRAP_CONTENT, // width\n            ViewGroup.LayoutParams.WRAP_CONTENT, // height\n        ).apply {\n            topMargin = resources.getDimensionPixelSize(R.dimen.bouncerButtonMargin)\n            bottomMargin = resources.getDimensionPixelSize(R.dimen.bouncerButtonMargin)\n            marginStart = resources.getDimensionPixelSize(R.dimen.bouncerButtonMargin)\n            marginEnd = resources.getDimensionPixelSize(R.dimen.bouncerButtonMargin)\n        }\n\n        torchButtonView.addConstraints {\n            connect(it.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)\n            connect(it.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)\n        }\n    }\n\n    protected open fun setupSwapCameraButtonViewConstraints() {\n        swapCameraButtonView.layoutParams = ConstraintLayout.LayoutParams(\n            ViewGroup.LayoutParams.WRAP_CONTENT, // width\n            ViewGroup.LayoutParams.WRAP_CONTENT, // height\n        ).apply {\n            topMargin = resources.getDimensionPixelSize(R.dimen.bouncerButtonMargin)\n            bottomMargin = resources.getDimensionPixelSize(R.dimen.bouncerButtonMargin)\n            marginStart = resources.getDimensionPixelSize(R.dimen.bouncerButtonMargin)\n            marginEnd = resources.getDimensionPixelSize(R.dimen.bouncerButtonMargin)\n        }\n\n        swapCameraButtonView.addConstraints {\n            connect(it.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)\n            connect(it.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)\n        }\n    }\n\n    protected open fun setupViewFinderConstraints() {\n        viewFinderBackgroundView.layoutParams = ConstraintLayout.LayoutParams(0, 0)\n\n        viewFinderBackgroundView.constrainToParent()\n\n        val screenSize = Resources.getSystem().displayMetrics.let {\n            Size(it.widthPixels, it.heightPixels)\n        }\n        val viewFinderMargin = (min(screenSize.width, screenSize.height) * getFloatResource(R.dimen.bouncerViewFinderMargin)).roundToInt()\n\n        listOf(viewFinderWindowView, viewFinderBorderView).forEach { view ->\n            view.layoutParams = ConstraintLayout.LayoutParams(0, 0).apply {\n                topMargin = viewFinderMargin\n                bottomMargin = viewFinderMargin\n                marginStart = viewFinderMargin\n                marginEnd = viewFinderMargin\n            }\n\n            view.constrainToParent()\n            view.addConstraints {\n                setVerticalBias(it.id, getFloatResource(R.dimen.bouncerViewFinderVerticalBias))\n                setHorizontalBias(it.id, getFloatResource(R.dimen.bouncerViewFinderHorizontalBias))\n\n                setDimensionRatio(it.id, viewFinderAspectRatio)\n            }\n        }\n    }\n\n    protected open fun setupInstructionsViewConstraints() {\n        instructionsTextView.layoutParams = ConstraintLayout.LayoutParams(\n            0, // width\n            ViewGroup.LayoutParams.WRAP_CONTENT, // height\n        ).apply {\n            topMargin = resources.getDimensionPixelSize(R.dimen.bouncerInstructionsMargin)\n            bottomMargin = resources.getDimensionPixelSize(R.dimen.bouncerInstructionsMargin)\n            marginStart = resources.getDimensionPixelSize(R.dimen.bouncerInstructionsMargin)\n            marginEnd = resources.getDimensionPixelSize(R.dimen.bouncerInstructionsMargin)\n        }\n\n        instructionsTextView.addConstraints {\n            connect(it.id, ConstraintSet.BOTTOM, viewFinderWindowView.id, ConstraintSet.TOP)\n            connect(it.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)\n            connect(it.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)\n        }\n    }\n\n    protected open fun setupSecurityNoticeConstraints() {\n        securityIconView.layoutParams = ConstraintLayout.LayoutParams(\n            ViewGroup.LayoutParams.WRAP_CONTENT, // width\n            0, // height\n        ).apply {\n            marginEnd = resources.getDimensionPixelSize(R.dimen.bouncerSecurityIconMargin)\n        }\n\n        securityTextView.layoutParams = ConstraintLayout.LayoutParams(\n            ViewGroup.LayoutParams.WRAP_CONTENT, // width\n            ViewGroup.LayoutParams.WRAP_CONTENT, // height\n        ).apply {\n            topMargin = resources.getDimensionPixelSize(R.dimen.bouncerSecurityMargin)\n            bottomMargin = resources.getDimensionPixelSize(R.dimen.bouncerSecurityMargin)\n        }\n\n        securityIconView.addConstraints {\n            connect(it.id, ConstraintSet.TOP, securityTextView.id, ConstraintSet.TOP)\n            connect(it.id, ConstraintSet.BOTTOM, securityTextView.id, ConstraintSet.BOTTOM)\n            connect(it.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)\n            connect(it.id, ConstraintSet.END, securityTextView.id, ConstraintSet.START)\n\n            setHorizontalChainStyle(it.id, ConstraintSet.CHAIN_PACKED)\n        }\n\n        securityTextView.addConstraints {\n            connect(it.id, ConstraintSet.TOP, viewFinderWindowView.id, ConstraintSet.BOTTOM)\n            connect(it.id, ConstraintSet.START, securityIconView.id, ConstraintSet.END)\n            connect(it.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)\n        }\n    }\n\n    protected open fun setupCardDetailsConstraints() {\n        cardNumberTextView.layoutParams = ConstraintLayout.LayoutParams(\n            0, // width\n            ViewGroup.LayoutParams.WRAP_CONTENT, // height\n        ).apply {\n            marginStart = resources.getDimensionPixelSize(R.dimen.bouncerCardDetailsMargin)\n            marginEnd = resources.getDimensionPixelSize(R.dimen.bouncerCardDetailsMargin)\n        }\n\n        cardNameTextView.layoutParams = ConstraintLayout.LayoutParams(\n            0, // width\n            ViewGroup.LayoutParams.WRAP_CONTENT, // height\n        ).apply {\n            marginStart = resources.getDimensionPixelSize(R.dimen.bouncerCardDetailsMargin)\n            marginEnd = resources.getDimensionPixelSize(R.dimen.bouncerCardDetailsMargin)\n\n            topToBottom = cardNumberTextView.id\n            bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID\n            startToStart = ConstraintLayout.LayoutParams.PARENT_ID\n            endToEnd = ConstraintLayout.LayoutParams.PARENT_ID\n        }\n\n        cardNumberTextView.addConstraints {\n            connect(it.id, ConstraintSet.TOP, viewFinderWindowView.id, ConstraintSet.TOP)\n            connect(it.id, ConstraintSet.BOTTOM, cardNameTextView.id, ConstraintSet.TOP)\n            connect(it.id, ConstraintSet.START, viewFinderWindowView.id, ConstraintSet.START)\n            connect(it.id, ConstraintSet.END, viewFinderWindowView.id, ConstraintSet.END)\n\n            setVerticalChainStyle(it.id, ConstraintSet.CHAIN_PACKED)\n        }\n\n        cardNameTextView.addConstraints {\n            connect(it.id, ConstraintSet.TOP, cardNumberTextView.id, ConstraintSet.BOTTOM)\n            connect(it.id, ConstraintSet.BOTTOM, viewFinderWindowView.id, ConstraintSet.BOTTOM)\n            connect(it.id, ConstraintSet.START, viewFinderWindowView.id, ConstraintSet.START)\n            connect(it.id, ConstraintSet.END, viewFinderWindowView.id, ConstraintSet.END)\n        }\n    }\n\n    protected open fun setupDebugConstraints() {\n        listOf(debugImageView, debugOverlayView).forEach { view ->\n            view.layoutParams = ConstraintLayout.LayoutParams(\n                resources.getDimensionPixelSize(R.dimen.bouncerDebugWindowWidth), // width\n                0, // height\n            )\n\n            view.addConstraints {\n                setDimensionRatio(it.id, viewFinderAspectRatio)\n\n                connect(it.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)\n                connect(it.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)\n            }\n        }\n    }\n\n    private fun setupLogoConstraints() {\n        logoView.layoutParams = ConstraintLayout.LayoutParams(\n            dpToPixels(LOGO_WIDTH_DP), // width\n            ViewGroup.LayoutParams.WRAP_CONTENT, // height\n        ).apply {\n            topMargin = resources.getDimensionPixelSize(R.dimen.bouncerLogoMargin)\n        }\n\n        logoView.addConstraints {\n            connect(it.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)\n            connect(it.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)\n            connect(it.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)\n        }\n    }\n\n    private fun setupVersionConstraints() {\n        versionTextView.layoutParams = ConstraintLayout.LayoutParams(\n            ViewGroup.LayoutParams.WRAP_CONTENT, // width\n            ViewGroup.LayoutParams.WRAP_CONTENT, // height\n        ).apply {\n            bottomMargin = resources.getDimensionPixelSize(R.dimen.bouncerLogoMargin)\n        }\n\n        versionTextView.addConstraints {\n            connect(it.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)\n            connect(it.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)\n            connect(it.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)\n        }\n    }\n\n    private var scanStatePrevious: ScanState? = null\n    protected var scanState: ScanState = ScanState.NotFound\n        private set\n\n    /**\n     * Change the state of the scanner.\n     */\n    protected fun changeScanState(newState: ScanState): Boolean {\n        if (newState == scanStatePrevious || scanStatePrevious?.isFinal == true) {\n            return false\n        }\n\n        scanState = newState\n        displayState(newState, scanStatePrevious)\n        scanStatePrevious = newState\n        return true\n    }\n\n    protected open fun displayState(newState: ScanState, previousState: ScanState?) {\n        when (newState) {\n            is ScanState.NotFound -> {\n                viewFinderBackgroundView.setBackgroundColor(getColorByRes(R.color.bouncerNotFoundBackground))\n                viewFinderWindowView.setBackgroundResource(R.drawable.bouncer_card_background_not_found)\n                viewFinderBorderView.startAnimation(R.drawable.bouncer_card_border_not_found)\n                instructionsTextView.setText(R.string.bouncer_card_scan_instructions)\n                cardNumberTextView.hide()\n                cardNameTextView.hide()\n            }\n            is ScanState.FoundShort -> {\n                viewFinderBackgroundView.setBackgroundColor(getColorByRes(R.color.bouncerFoundBackground))\n                viewFinderWindowView.setBackgroundResource(R.drawable.bouncer_card_background_found)\n                viewFinderBorderView.startAnimation(R.drawable.bouncer_card_border_found)\n                instructionsTextView.setText(R.string.bouncer_card_scan_instructions)\n                instructionsTextView.show()\n            }\n            is ScanState.FoundLong -> {\n                viewFinderBackgroundView.setBackgroundColor(getColorByRes(R.color.bouncerFoundBackground))\n                viewFinderWindowView.setBackgroundResource(R.drawable.bouncer_card_background_found)\n                viewFinderBorderView.startAnimation(R.drawable.bouncer_card_border_found_long)\n                instructionsTextView.setText(R.string.bouncer_card_scan_instructions)\n                instructionsTextView.show()\n            }\n            is ScanState.Correct -> {\n                viewFinderBackgroundView.setBackgroundColor(getColorByRes(R.color.bouncerCorrectBackground))\n                viewFinderWindowView.setBackgroundResource(R.drawable.bouncer_card_background_correct)\n                viewFinderBorderView.startAnimation(R.drawable.bouncer_card_border_correct)\n                instructionsTextView.hide()\n            }\n            is ScanState.Wrong -> {\n                viewFinderBackgroundView.setBackgroundColor(getColorByRes(R.color.bouncerWrongBackground))\n                viewFinderWindowView.setBackgroundResource(R.drawable.bouncer_card_background_wrong)\n                viewFinderBorderView.startAnimation(R.drawable.bouncer_card_border_wrong)\n                instructionsTextView.setText(R.string.bouncer_scanned_wrong_card)\n            }\n        }\n    }\n\n    override fun onFlashlightStateChanged(flashlightOn: Boolean) {\n        setupUiComponents()\n    }\n\n    override fun prepareCamera(onCameraReady: () -> Unit) {\n        previewFrame.post {\n            viewFinderBackgroundView.setViewFinderRect(viewFinderWindowView.asRect())\n            onCameraReady()\n        }\n    }\n\n    override fun onFlashSupported(supported: Boolean) {\n        isFlashlightSupported = supported\n        torchButtonView.setVisible(supported)\n    }\n\n    override fun onSupportsMultipleCameras(supported: Boolean) {\n        hasMultipleCameras = supported\n        swapCameraButtonView.setVisible(supported)\n    }\n\n    /**\n     * Add constraints to a view.\n     */\n    protected inline fun <T : View> T.addConstraints(block: ConstraintSet.(view: T) -> Unit) {\n        ConstraintSet().apply {\n            clone(layout)\n            block(this, this@addConstraints)\n            applyTo(layout)\n        }\n    }\n\n    /**\n     * Constrain a view to the top, bottom, start, and end of its parent.\n     */\n    protected fun <T : View> T.constrainToParent() {\n        addConstraints {\n            connect(it.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)\n            connect(it.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)\n            connect(it.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)\n            connect(it.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)\n        }\n    }\n\n    /**\n     * Once the camera stream is available, start processing images.\n     */\n    override fun onCameraStreamAvailable(cameraStream: Flow<CameraPreviewImage<Bitmap>>) {\n        scanFlow.startFlow(\n            context = this,\n            imageStream = cameraStream,\n            viewFinder = viewFinderWindowView.asRect(),\n            lifecycleOwner = this,\n            coroutineScope = this,\n        )\n    }\n\n    override fun onInvalidApiKey() {\n        scanFlow.cancelFlow()\n    }\n}\n"
  },
  {
    "path": "scan-ui/src/main/java/com/getbouncer/scan/ui/ViewFinderBackground.kt",
    "content": "package com.getbouncer.scan.ui\n\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.Paint\nimport android.graphics.PorterDuff\nimport android.graphics.PorterDuffXfermode\nimport android.graphics.Rect\nimport android.os.Build\nimport android.util.AttributeSet\nimport android.view.View\nimport androidx.annotation.ColorInt\nimport kotlin.math.roundToInt\n\n/**\n * This class draws a background with a hole in the middle of it.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nclass ViewFinderBackground(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {\n    private var viewFinderRect: Rect? = null\n    private var onDrawListener: (() -> Unit)? = null\n\n    fun setViewFinderRect(viewFinderRect: Rect) {\n        this.viewFinderRect = viewFinderRect\n        requestLayout()\n    }\n\n    fun clearViewFinderRect() {\n        this.viewFinderRect = null\n    }\n\n    override fun setBackgroundColor(@ColorInt color: Int) {\n        paintBackground.color = color\n        requestLayout()\n    }\n\n    fun getBackgroundLuminance(): Int {\n        val color = paintBackground.color\n        val r = (color shr 16 and 0xff) / 255F\n        val g = (color shr 8 and 0xff) / 255F\n        val b = (color and 0xff) / 255F\n\n        return ((0.2126F * r + 0.7152F * g + 0.0722F * b) * 255F).roundToInt()\n    }\n\n    fun setOnDrawListener(onDrawListener: () -> Unit) {\n        this.onDrawListener = onDrawListener\n    }\n\n    fun clearOnDrawListener() {\n        this.onDrawListener = null\n    }\n\n    private val theme = context.theme\n    private val attributes = theme.obtainStyledAttributes(attrs, R.styleable.ViewFinderBackground, 0, 0)\n    private val backgroundColor =\n        attributes.getColor(\n            R.styleable.ViewFinderBackground_backgroundColor,\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n                resources.getColor(R.color.bouncerNotFoundBackground, theme)\n            } else {\n                @Suppress(\"deprecation\")\n                resources.getColor(R.color.bouncerNotFoundBackground)\n            }\n        )\n\n    private var paintBackground = Paint(Paint.ANTI_ALIAS_FLAG).apply {\n        color = backgroundColor\n        style = Paint.Style.FILL\n    }\n\n    private val paintWindow = Paint(Paint.ANTI_ALIAS_FLAG).apply {\n        xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)\n        style = Paint.Style.FILL\n    }\n\n    init {\n        setLayerType(LAYER_TYPE_HARDWARE, null)\n    }\n\n    override fun onDraw(canvas: Canvas) {\n        super.onDraw(canvas)\n        canvas.drawPaint(paintBackground)\n\n        val viewFinderRect = this.viewFinderRect\n        if (viewFinderRect != null) {\n            canvas.drawRect(viewFinderRect, paintWindow)\n        }\n\n        val onDrawListener = this.onDrawListener\n        if (onDrawListener != null) {\n            onDrawListener()\n        }\n    }\n}\n"
  },
  {
    "path": "scan-ui/src/main/java/com/getbouncer/scan/ui/util/ViewExtensions.kt",
    "content": "package com.getbouncer.scan.ui.util\n\nimport android.content.Context\nimport android.content.res.Resources.NotFoundException\nimport android.graphics.PointF\nimport android.graphics.Rect\nimport android.graphics.drawable.Animatable\nimport android.util.TypedValue\nimport android.view.View\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.annotation.ColorInt\nimport androidx.annotation.ColorRes\nimport androidx.annotation.DimenRes\nimport androidx.annotation.DrawableRes\nimport androidx.core.content.ContextCompat\nimport kotlin.math.roundToInt\n\n/**\n * Determine if a view is visible.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun View.isVisible() = this.visibility == View.VISIBLE\n\n/**\n * Set a view's visibility.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun View.setVisible(visible: Boolean) {\n    this.visibility = if (visible) View.VISIBLE else View.GONE\n}\n\n/**\n * Make a view visible.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun View.show() = setVisible(true)\n\n/**\n * Make a view invisible.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun View.hide() = setVisible(false)\n\n/**\n * Get a [ColorInt] from a [ColorRes].\n */\n@ColorInt\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Context.getColorByRes(@ColorRes colorRes: Int) = ContextCompat.getColor(this, colorRes)\n\n/**\n * Get a [Drawable] from a [DrawableRes]\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Context.getDrawableByRes(@DrawableRes drawableRes: Int) = ContextCompat.getDrawable(\n    this,\n    drawableRes\n)\n\n/**\n * Set the image of an [ImageView] using a [DrawableRes].\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun ImageView.setDrawable(@DrawableRes drawableRes: Int) {\n    this.setImageDrawable(this.context.getDrawableByRes(drawableRes))\n}\n\n/**\n * Set the image of an [ImageView] using a [DrawableRes] and start the animation.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun ImageView.startAnimation(@DrawableRes drawableRes: Int) {\n    val d = this.context.getDrawableByRes(drawableRes)\n    setImageDrawable(d)\n    if (d is Animatable) {\n        d.start()\n    }\n}\n\n/**\n * Get a rect from a view.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun View.asRect() = Rect(left, top, right, bottom)\n\n/**\n * Convert an int in DP to pixels.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Context.dpToPixels(dp: Int) = (dp * resources.displayMetrics.density).roundToInt()\n\n/**\n * This is copied from Resources.java for API 29 so that we can continue to support API 21.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun Context.getFloatResource(@DimenRes id: Int): Float {\n    val value = TypedValue()\n    resources.getValue(id, value, true)\n    if (value.type == TypedValue.TYPE_FLOAT) {\n        return value.float\n    }\n    throw NotFoundException(\"Resource ID #0x ${Integer.toHexString(id)} type #0x${Integer.toHexString(value.type)} is not valid\")\n}\n\n/**\n * Set the size of a text field using a dimension.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun TextView.setTextSizeByRes(@DimenRes id: Int) {\n    setTextSize(TypedValue.COMPLEX_UNIT_PX, this.resources.getDimension(id))\n}\n\n/**\n * Determine the center point of a view.\n */\n@Deprecated(\n    message = \"Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan\",\n    replaceWith = ReplaceWith(\"StripeCardScan\"),\n)\nfun View.centerPoint() = PointF(left + width / 2F, top + height / 2F)\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_camera_swap_dark.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"64.0\"\n    android:viewportHeight=\"64.0\">\n\n    <path\n        android:pathData=\"M41.2,25.4h-4v-1.7c0-1.2-1-2.2-2.2-2.2H29c-1.2,0-2.2,1-2.2,2.2v1.7h-4c-1.2,0-2.2,1-2.2,2.2v12.7c0,1.2,1,2.2,2.2,2.2h18.4c1.2,0,2.2-1,2.2-2.2V27.6C43.4,26.4,42.4,25.4,41.2,25.4z\"\n        android:strokeColor=\"@color/bouncerCameraSwapButtonDarkColor\"\n        android:strokeWidth=\"@integer/bouncerIconStrokeWidth\"\n        android:strokeLineJoin=\"round\" />\n\n    <path\n        android:pathData=\"M38.1,33.8c0-3.4-2.8-6.1-6.1-6.1s-6.1,2.8-6.1,6.1c0,3.4,2.8,6.1,6.1,6.1\"\n        android:strokeColor=\"@color/bouncerCameraSwapButtonDarkColor\"\n        android:strokeWidth=\"@integer/bouncerIconStrokeWidth\"\n        android:strokeLineCap=\"round\" />\n\n    <path\n        android:pathData=\"M40.1,32.4L38.1,33.8L35.8,32.7\"\n        android:strokeWidth=\"@integer/bouncerIconStrokeWidth\"\n        android:strokeColor=\"@color/bouncerCameraSwapButtonDarkColor\"\n        android:strokeLineCap=\"round\"\n        android:strokeLineJoin=\"round\" />\n\n</vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_camera_swap_light.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"64.0\"\n    android:viewportHeight=\"64.0\">\n\n    <path\n        android:pathData=\"M41.2,25.4h-4v-1.7c0-1.2-1-2.2-2.2-2.2H29c-1.2,0-2.2,1-2.2,2.2v1.7h-4c-1.2,0-2.2,1-2.2,2.2v12.7c0,1.2,1,2.2,2.2,2.2h18.4c1.2,0,2.2-1,2.2-2.2V27.6C43.4,26.4,42.4,25.4,41.2,25.4z\"\n        android:strokeColor=\"@color/bouncerCameraSwapButtonLightColor\"\n        android:strokeWidth=\"@integer/bouncerIconStrokeWidth\"\n        android:strokeLineJoin=\"round\" />\n\n    <path\n        android:pathData=\"M38.1,33.8c0-3.4-2.8-6.1-6.1-6.1s-6.1,2.8-6.1,6.1c0,3.4,2.8,6.1,6.1,6.1\"\n        android:strokeColor=\"@color/bouncerCameraSwapButtonLightColor\"\n        android:strokeWidth=\"@integer/bouncerIconStrokeWidth\"\n        android:strokeLineCap=\"round\" />\n\n    <path\n        android:pathData=\"M40.1,32.4L38.1,33.8L35.8,32.7\"\n        android:strokeWidth=\"@integer/bouncerIconStrokeWidth\"\n        android:strokeColor=\"@color/bouncerCameraSwapButtonLightColor\"\n        android:strokeLineCap=\"round\"\n        android:strokeLineJoin=\"round\" />\n\n</vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_card_background_correct.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:name=\"vector\"\n    android:width=\"200dp\"\n    android:height=\"126dp\"\n    android:viewportWidth=\"200\"\n    android:viewportHeight=\"126\">\n    <path\n        android:name=\"card_border\"\n        android:pathData=\"\n            M 2.5,20.5\n            L 2.5,7.5\n            a 5,5,0,0,1,5,-5\n            L 192.5,2.5\n            a 5,5,0,0,1,5,5\n            L 197.5,118.5\n            a 5,5,0,0,1,-5,5\n            L 7.5,123.5\n            a 5,5,0,0,1,-5,-5\n            L 2.5,20.5\n            L 0,20.5\n            L 0,126\n            L 200,126\n            L 200,0\n            L 0,0\n            L 0,20.5\n            Z\"\n        android:fillColor=\"@color/bouncerCorrectBackground\"/>\n</vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_card_background_found.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:name=\"vector\"\n    android:width=\"200dp\"\n    android:height=\"126dp\"\n    android:viewportWidth=\"200\"\n    android:viewportHeight=\"126\">\n    <path\n        android:name=\"card_border\"\n        android:pathData=\"\n            M 2.5,20.5\n            L 2.5,7.5\n            a 5,5,0,0,1,5,-5\n            L 192.5,2.5\n            a 5,5,0,0,1,5,5\n            L 197.5,118.5\n            a 5,5,0,0,1,-5,5\n            L 7.5,123.5\n            a 5,5,0,0,1,-5,-5\n            L 2.5,20.5\n            L 0,20.5\n            L 0,126\n            L 200,126\n            L 200,0\n            L 0,0\n            L 0,20.5\n            Z\"\n        android:fillColor=\"@color/bouncerFoundBackground\"/>\n</vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_card_background_not_found.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:name=\"vector\"\n    android:width=\"200dp\"\n    android:height=\"126dp\"\n    android:viewportWidth=\"200\"\n    android:viewportHeight=\"126\">\n    <path\n        android:name=\"card_border\"\n        android:pathData=\"\n            M 2.5,20.5\n            L 2.5,7.5\n            a 5,5,0,0,1,5,-5\n            L 192.5,2.5\n            a 5,5,0,0,1,5,5\n            L 197.5,118.5\n            a 5,5,0,0,1,-5,5\n            L 7.5,123.5\n            a 5,5,0,0,1,-5,-5\n            L 2.5,20.5\n            L 0,20.5\n            L 0,126\n            L 200,126\n            L 200,0\n            L 0,0\n            L 0,20.5\n            Z\"\n        android:fillColor=\"@color/bouncerNotFoundBackground\"/>\n</vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_card_background_wrong.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:name=\"vector\"\n    android:width=\"200dp\"\n    android:height=\"126dp\"\n    android:viewportWidth=\"200\"\n    android:viewportHeight=\"126\">\n    <path\n        android:name=\"card_border\"\n        android:pathData=\"\n            M 2.5,20.5\n            L 2.5,7.5\n            a 5,5,0,0,1,5,-5\n            L 192.5,2.5\n            a 5,5,0,0,1,5,5\n            L 197.5,118.5\n            a 5,5,0,0,1,-5,5\n            L 7.5,123.5\n            a 5,5,0,0,1,-5,-5\n            L 2.5,20.5\n            L 0,20.5\n            L 0,126\n            L 200,126\n            L 200,0\n            L 0,0\n            L 0,20.5\n            Z\"\n        android:fillColor=\"@color/bouncerWrongBackground\"/>\n</vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_card_border_correct.xml",
    "content": "<animated-vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\">\n    <aapt:attr name=\"android:drawable\">\n        <vector\n            android:name=\"vector\"\n            android:width=\"200dp\"\n            android:height=\"126dp\"\n            android:viewportWidth=\"200\"\n            android:viewportHeight=\"126\">\n            <path\n                android:name=\"card_border\"\n                android:pathData=\"\n                    M 2.5,20.5\n                    L 2.5,7.5\n                    a 5,5,0,0,1,5,-5\n                    L 192.5,2.5\n                    a 5,5,0,0,1,5,5\n                    L 197.5,118.5\n                    a 5,5,0,0,1,-5,5\n                    L 7.5,123.5\n                    a 5,5,0,0,1,-5,-5\n                    Z\"\n                android:strokeColor=\"@color/bouncerCorrectOutline\"\n                android:strokeWidth=\"@integer/bouncerFoundOutlineWidth\"/>\n        </vector>\n    </aapt:attr>\n    <target android:name=\"card_border\">\n        <aapt:attr name=\"android:animation\">\n            <set>\n                <objectAnimator\n                    android:propertyName=\"strokeWidth\"\n                    android:duration=\"250\"\n                    android:valueFrom=\"@integer/bouncerFoundOutlineWidth\"\n                    android:valueTo=\"@integer/bouncerCorrectOutlineWidth\"\n                    android:valueType=\"floatType\"\n                    android:interpolator=\"@android:interpolator/linear_out_slow_in\"/>\n                <objectAnimator\n                    android:propertyName=\"strokeWidth\"\n                    android:startOffset=\"250\"\n                    android:duration=\"250\"\n                    android:valueFrom=\"@integer/bouncerCorrectOutlineWidth\"\n                    android:valueTo=\"@integer/bouncerNotFoundOutlineWidth\"\n                    android:valueType=\"floatType\"\n                    android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n            </set>\n        </aapt:attr>\n    </target>\n</animated-vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_card_border_found.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:name=\"vector\"\n    android:width=\"200dp\"\n    android:height=\"126dp\"\n    android:viewportWidth=\"200\"\n    android:viewportHeight=\"126\">\n    <path\n        android:name=\"card_border\"\n        android:pathData=\"\n            M 2.5,20.5\n            L 2.5,7.5\n            a 5,5,0,0,1,5,-5\n            L 192.5,2.5\n            a 5,5,0,0,1,5,5\n            L 197.5,118.5\n            a 5,5,0,0,1,-5,5\n            L 7.5,123.5\n            a 5,5,0,0,1,-5,-5\n            Z\"\n        android:strokeColor=\"@color/bouncerNotFoundOutline\"\n        android:strokeWidth=\"@integer/bouncerNotFoundOutlineWidth\"/>\n</vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_card_border_found_long.xml",
    "content": "<animated-vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\">\n    <aapt:attr name=\"android:drawable\">\n        <vector\n            android:name=\"vector\"\n            android:width=\"200dp\"\n            android:height=\"126dp\"\n            android:viewportWidth=\"200\"\n            android:viewportHeight=\"126\">\n            <path\n                android:name=\"card_border\"\n                android:pathData=\"\n                        M 2.5,20.5\n                        L 2.5,7.5\n                        a 5,5,0,0,1,5,-5\n                        L 192.5,2.5\n                        a 5,5,0,0,1,5,5\n                        L 197.5,118.5\n                        a 5,5,0,0,1,-5,5\n                        L 7.5,123.5\n                        a 5,5,0,0,1,-5,-5\n                        Z\"\n                android:strokeColor=\"@color/bouncerFoundOutline\"\n                android:trimPathEnd=\"0\"\n                android:strokeLineCap=\"round\"\n                android:strokeWidth=\"@integer/bouncerFoundOutlineWidth\"/>\n        </vector>\n    </aapt:attr>\n    <target android:name=\"card_border\">\n        <aapt:attr name=\"android:animation\">\n            <objectAnimator\n                android:propertyName=\"trimPathEnd\"\n                android:duration=\"15000\"\n                android:valueFrom=\"0\"\n                android:valueTo=\"1\"\n                android:valueType=\"floatType\"\n                android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n        </aapt:attr>\n    </target>\n</animated-vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_card_border_not_found.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:name=\"vector\"\n    android:width=\"200dp\"\n    android:height=\"126dp\"\n    android:viewportWidth=\"200\"\n    android:viewportHeight=\"126\">\n    <path\n        android:name=\"card_border\"\n        android:pathData=\"\n            M 2.5,20.5\n            L 2.5,7.5\n            a 5,5,0,0,1,5,-5\n            L 192.5,2.5\n            a 5,5,0,0,1,5,5\n            L 197.5,118.5\n            a 5,5,0,0,1,-5,5\n            L 7.5,123.5\n            a 5,5,0,0,1,-5,-5\n            Z\"\n        android:strokeColor=\"@color/bouncerNotFoundOutline\"\n        android:strokeWidth=\"@integer/bouncerNotFoundOutlineWidth\"/>\n</vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_card_border_wrong.xml",
    "content": "<animated-vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\">\n    <aapt:attr name=\"android:drawable\">\n        <vector\n            android:name=\"vector\"\n            android:width=\"200dp\"\n            android:height=\"126dp\"\n            android:viewportWidth=\"200\"\n            android:viewportHeight=\"126\">\n            <path\n                android:name=\"card_border\"\n                android:pathData=\"\n                    M 2.5,20.5\n                    L 2.5,7.5\n                    a 5,5,0,0,1,5,-5\n                    L 192.5,2.5\n                    a 5,5,0,0,1,5,5\n                    L 197.5,118.5\n                    a 5,5,0,0,1,-5,5\n                    L 7.5,123.5\n                    a 5,5,0,0,1,-5,-5\n                    Z\"\n                android:strokeColor=\"@color/bouncerWrongOutline\"\n                android:strokeWidth=\"@integer/bouncerNotFoundOutlineWidth\"/>\n        </vector>\n    </aapt:attr>\n    <target android:name=\"card_border\">\n        <aapt:attr name=\"android:animation\">\n            <set>\n                <objectAnimator\n                    android:propertyName=\"strokeWidth\"\n                    android:duration=\"250\"\n                    android:valueFrom=\"@integer/bouncerNotFoundOutlineWidth\"\n                    android:valueTo=\"@integer/bouncerWrongOutlineWidth\"\n                    android:valueType=\"floatType\"\n                    android:interpolator=\"@android:interpolator/linear_out_slow_in\"/>\n                <objectAnimator\n                    android:propertyName=\"strokeWidth\"\n                    android:startOffset=\"250\"\n                    android:duration=\"250\"\n                    android:valueFrom=\"@integer/bouncerWrongOutlineWidth\"\n                    android:valueTo=\"@integer/bouncerNotFoundOutlineWidth\"\n                    android:valueType=\"floatType\"\n                    android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n            </set>\n        </aapt:attr>\n    </target>\n</animated-vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_close_button_dark.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"64dp\"\n        android:height=\"64dp\"\n        android:viewportWidth=\"100.0\"\n        android:viewportHeight=\"100.0\">\n    <path\n        android:pathData=\"\n           M 36,36\n           L 64,64\n           M 36,64\n           L 64,36\"\n        android:strokeColor=\"@color/bouncerCloseButtonDarkColor\"\n        android:strokeWidth=\"@integer/bouncerIconStrokeWidth\"\n        android:strokeLineCap=\"round\" />\n</vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_close_button_light.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"64dp\"\n        android:height=\"64dp\"\n        android:viewportWidth=\"100.0\"\n        android:viewportHeight=\"100.0\">\n    <path\n        android:pathData=\"\n           M 36,36\n           L 64,64\n           M 36,64\n           L 64,36\"\n        android:strokeColor=\"@color/bouncerCloseButtonLightColor\"\n        android:strokeWidth=\"@integer/bouncerIconStrokeWidth\"\n        android:strokeLineCap=\"round\" />\n</vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_flash_off_dark.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"64dp\"\n        android:height=\"64dp\"\n        android:viewportWidth=\"100.0\"\n        android:viewportHeight=\"100.0\">\n    <path\n        android:pathData=\"\n           M 44,78\n           L 47,53\n           L 36,56\n           L 56,22\n           L 53,47\n           L 64,44 Z\"\n        android:strokeColor=\"@color/bouncerFlashButtonDarkColor\"\n        android:strokeWidth=\"@integer/bouncerIconStrokeWidth\"\n        android:strokeLineJoin=\"round\" />\n\n</vector>"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_flash_off_light.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"64dp\"\n        android:height=\"64dp\"\n        android:viewportWidth=\"100.0\"\n        android:viewportHeight=\"100.0\">\n    <path\n        android:pathData=\"\n           M 44,78\n           L 47,53\n           L 36,56\n           L 56,22\n           L 53,47\n           L 64,44 Z\"\n        android:strokeColor=\"@color/bouncerFlashButtonLightColor\"\n        android:strokeWidth=\"@integer/bouncerIconStrokeWidth\"\n        android:strokeLineJoin=\"round\" />\n\n</vector>"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_flash_on_dark.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"64dp\"\n        android:height=\"64dp\"\n        android:viewportWidth=\"100.0\"\n        android:viewportHeight=\"100.0\">\n    <path\n        android:pathData=\"\n           M 44,78\n           L 47,53\n           L 36,56\n           L 56,22\n           L 53,47\n           L 64,44 Z\"\n        android:fillColor=\"@color/bouncerFlashButtonDarkColor\"\n        android:strokeColor=\"@color/bouncerFlashButtonDarkColor\"\n        android:strokeWidth=\"@integer/bouncerIconStrokeWidth\"\n        android:strokeLineJoin=\"round\" />\n\n</vector>"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_flash_on_light.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"64dp\"\n        android:height=\"64dp\"\n        android:viewportWidth=\"100.0\"\n        android:viewportHeight=\"100.0\">\n    <path\n        android:pathData=\"\n           M 44,78\n           L 47,53\n           L 36,56\n           L 56,22\n           L 53,47\n           L 64,44 Z\"\n        android:fillColor=\"@color/bouncerFlashButtonLightColor\"\n        android:strokeColor=\"@color/bouncerFlashButtonLightColor\"\n        android:strokeWidth=\"@integer/bouncerIconStrokeWidth\"\n        android:strokeLineJoin=\"round\" />\n\n</vector>"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_lock_dark.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:name=\"vector\"\n    android:width=\"14dp\"\n    android:height=\"14dp\"\n    android:viewportWidth=\"48\"\n    android:viewportHeight=\"48\">\n\n    <path\n        android:name=\"lock_body\"\n        android:pathData=\"\n            M 24,17.5\n            l 10,0\n            a 5,5,0,0,1,5,5\n            l 0,18\n            a 5,5,0,0,1,-5,5\n            l -20,0\n            a 5,5,0,0,1,-5,-5\n            l 0,-18\n            a 5,5,0,0,1,5,-5\n            l 10,0\n            l 0,6.5\n            a 4,4,0,0,0,-1,7.873\n            l -1,6\n            l 4,0\n            l -1,-6\n            a 4,4,0,0,0,-1,-7.873\n            Z\"\n        android:fillColor=\"@color/bouncerSecurityColorDark\" />\n\n    <path\n        android:name=\"lock_shank\"\n        android:pathData=\"\n            M 15,18\n            l 0,-5.5\n            a 9,9,0,0,1,18,0\n            l 0,5\"\n        android:strokeColor=\"@color/bouncerSecurityColorDark\"\n        android:strokeWidth=\"2\"/>\n</vector>"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_lock_light.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:name=\"vector\"\n    android:width=\"14dp\"\n    android:height=\"14dp\"\n    android:viewportWidth=\"48\"\n    android:viewportHeight=\"48\">\n\n    <path\n        android:name=\"lock_body\"\n        android:pathData=\"\n            M 24,17.5\n            l 10,0\n            a 5,5,0,0,1,5,5\n            l 0,18\n            a 5,5,0,0,1,-5,5\n            l -20,0\n            a 5,5,0,0,1,-5,-5\n            l 0,-18\n            a 5,5,0,0,1,5,-5\n            l 10,0\n            l 0,6.5\n            a 4,4,0,0,0,-1,7.873\n            l -1,6\n            l 4,0\n            l -1,-6\n            a 4,4,0,0,0,-1,-7.873\n            Z\"\n        android:fillColor=\"@color/bouncerSecurityColorLight\" />\n\n    <path\n        android:name=\"lock_shank\"\n        android:pathData=\"\n            M 15,18\n            l 0,-5.5\n            a 9,9,0,0,1,18,0\n            l 0,5\"\n        android:strokeColor=\"@color/bouncerSecurityColorLight\"\n        android:strokeWidth=\"2\"/>\n</vector>"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_logo_dark_background.xml",
    "content": "<vector android:autoMirrored=\"true\" android:height=\"29dp\"\n    android:viewportHeight=\"7.672917\" android:viewportWidth=\"52.916664\"\n    android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillAlpha=\"1\" android:fillColor=\"#849cfe\" android:pathData=\"m2.5003,2.6989q0.9406,0 1.5765,0.5226 0.6358,0.5139 0.8013,1.4197H3.3016Q3.232,4.3276 3.0142,4.1534 2.7965,3.9792 2.4655,3.9792q-0.3919,0 -0.6445,0.3135 -0.2526,0.3048 -0.2526,0.8971 0,0.5923 0.2526,0.9058 0.2526,0.3048 0.6445,0.3048 0.331,0 0.5487,-0.1742Q3.232,6.0521 3.3016,5.7386H4.8781Q4.7126,6.6444 4.0768,7.167 3.441,7.6809 2.5003,7.6809q-0.7142,0 -1.2716,-0.2961Q0.68,7.0799 0.3665,6.5138 0.0529,5.9476 0.0529,5.1899q0,-0.7665 0.3048,-1.3239Q0.6713,3.2999 1.2287,3.0037 1.7861,2.6989 2.5003,2.6989Z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#7b9ffe\" android:pathData=\"m7.4991,2.6724q0.5574,0 0.9581,0.2526 0.4006,0.2526 0.5923,0.6881V2.7334H10.5301V7.5934H9.0494V6.7137Q8.8578,7.1492 8.4572,7.4018 8.0565,7.6544 7.4991,7.6544q-0.601,0 -1.08,-0.2961 -0.4703,-0.3048 -0.749,-0.871 -0.27,-0.5661 -0.27,-1.3239 0,-0.7665 0.27,-1.3239Q5.9488,3.2734 6.4191,2.9773 6.8981,2.6724 7.4991,2.6724ZM7.9781,3.9789q-0.479,0 -0.7752,0.3135 -0.2874,0.3135 -0.2874,0.871 0,0.5574 0.2874,0.871 0.2961,0.3135 0.7752,0.3135 0.4703,0 0.7665,-0.3223 0.3048,-0.3223 0.3048,-0.8623 0,-0.5487 -0.3048,-0.8623Q8.4485,3.9789 7.9781,3.9789Z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#74a3fe\" android:pathData=\"M13.0183,3.6744Q13.297,3.2302 13.7325,2.9689 14.1767,2.7076 14.6906,2.7076v1.5939h-0.4268q-0.5923,0 -0.9232,0.2526Q13.0183,4.8066 13.0183,5.4076V7.6199H11.529V2.7599h1.4894z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#6da6fe\" android:pathData=\"m17.188,2.6989q0.5574,0 0.9581,0.2526 0.4006,0.2526 0.5835,0.6881v-2.4648h1.4894L20.219,7.6199L18.7296,7.6199L18.7296,6.7402q-0.1829,0.4355 -0.5835,0.6881 -0.4006,0.2526 -0.9581,0.2526 -0.601,0 -1.08,-0.2961 -0.4703,-0.3048 -0.749,-0.871 -0.27,-0.5661 -0.27,-1.3239 0,-0.7665 0.27,-1.3239 0.2787,-0.5661 0.749,-0.8623 0.479,-0.3048 1.08,-0.3048zM17.667,4.0054q-0.479,0 -0.7752,0.3135 -0.2874,0.3135 -0.2874,0.871 0,0.5574 0.2874,0.871 0.2961,0.3135 0.7752,0.3135 0.4703,0 0.7665,-0.3223 0.3048,-0.3223 0.3048,-0.8623 0,-0.5487 -0.3048,-0.8623 -0.2961,-0.3223 -0.7665,-0.3223z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#66aafe\" android:pathData=\"m22.9859,2.6989q0.9232,0 1.4719,0.4616 0.5574,0.4616 0.6968,1.2194H23.7611Q23.7001,4.0837 23.4824,3.9183 23.2733,3.7441 22.9511,3.7441q-0.2526,0 -0.3832,0.1132 -0.1306,0.1045 -0.1306,0.3048 0,0.2265 0.2352,0.3397 0.2439,0.1132 0.7577,0.2265 0.5574,0.1306 0.9145,0.27 0.3571,0.1306 0.6184,0.4355 0.2613,0.3048 0.2613,0.8187 0,0.4181 -0.2265,0.7403 -0.2265,0.3223 -0.6532,0.5052 -0.4268,0.1829 -1.0103,0.1829 -0.9842,0 -1.5765,-0.4355Q21.1656,6.8099 21.035,5.9999h1.4371q0.0348,0.3135 0.2613,0.479 0.2352,0.1655 0.601,0.1655 0.2526,0 0.3832,-0.1132 0.1306,-0.1219 0.1306,-0.3135 0,-0.2526 -0.2439,-0.3571Q23.3692,5.7473 22.8292,5.6254 22.2892,5.5121 21.9408,5.3815 21.5924,5.2508 21.3398,4.9634 21.0872,4.6673 21.0872,4.1621q0,-0.6532 0.4965,-1.0539 0.4965,-0.4094 1.4023,-0.4094z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#59bce9\" android:pathData=\"m28.2836,2.6989q0.9406,0 1.5765,0.5226 0.6358,0.5139 0.8013,1.4197H29.0849Q29.0152,4.3276 28.7975,4.1534 28.5797,3.9792 28.2488,3.9792q-0.3919,0 -0.6445,0.3135 -0.2526,0.3048 -0.2526,0.8971 0,0.5923 0.2526,0.9058 0.2526,0.3048 0.6445,0.3048 0.331,0 0.5487,-0.1742 0.2177,-0.1742 0.2874,-0.4877h1.5765q-0.1655,0.9058 -0.8013,1.4284 -0.6358,0.5139 -1.5765,0.5139 -0.7142,0 -1.2716,-0.2961 -0.5487,-0.3048 -0.8623,-0.871 -0.3135,-0.5661 -0.3135,-1.3239 0,-0.7665 0.3048,-1.3239 0.3135,-0.5661 0.871,-0.8623 0.5574,-0.3048 1.2716,-0.3048z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#49d3cc\" android:pathData=\"m33.2824,2.6989q0.5574,0 0.9581,0.2526 0.4006,0.2526 0.5923,0.6881L34.8327,2.7599L36.3134,2.7599L36.3134,7.6199L34.8327,7.6199L34.8327,6.7402q-0.1916,0.4355 -0.5923,0.6881 -0.4006,0.2526 -0.9581,0.2526 -0.601,0 -1.08,-0.2961Q31.732,7.0799 31.4533,6.5138 31.1833,5.9476 31.1833,5.1899q0,-0.7665 0.27,-1.3239Q31.732,3.2999 32.2024,3.0037 32.6814,2.6989 33.2824,2.6989ZM33.7614,4.0054q-0.479,0 -0.7752,0.3135 -0.2874,0.3135 -0.2874,0.871 0,0.5574 0.2874,0.871 0.2961,0.3135 0.7752,0.3135 0.4703,0 0.7665,-0.3223 0.3048,-0.3223 0.3048,-0.8623 0,-0.5487 -0.3048,-0.8623 -0.2961,-0.3223 -0.7665,-0.3223z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#39eab0\" android:pathData=\"m40.3955,2.7076q0.8361,0 1.3239,0.5574 0.4965,0.5574 0.4965,1.5329V7.6199H40.7264V4.9721q0,-0.4877 -0.2613,-0.7577 -0.2526,-0.2787 -0.6794,-0.2787 -0.4529,0 -0.7229,0.2874 -0.2613,0.2874 -0.2613,0.8187v2.5781h-1.4894V2.7599h1.4894v0.8884q0.209,-0.4355 0.6184,-0.6881 0.4181,-0.2526 0.9755,-0.2526z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#30fa9e\" android:pathData=\"M44.4953,6.1305V7.6199H42.9537V6.1305Z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#30fa9e\" android:pathData=\"m45.1155,1.3924q0,-0.3397 0.2439,-0.5574 0.2439,-0.2265 0.6445,-0.2265 0.4006,0 0.6445,0.2265 0.2439,0.2177 0.2439,0.5574 0,0.331 -0.2439,0.5574 -0.2439,0.2177 -0.6445,0.2177 -0.4006,0 -0.6445,-0.2177 -0.2439,-0.2265 -0.2439,-0.5574zM46.7442,2.7599L46.7442,7.6199L45.2549,7.6199L45.2549,2.7599Z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#30fa9e\" android:pathData=\"m50.0442,2.6989q0.7229,0 1.289,0.3048 0.5748,0.2961 0.8971,0.8623 0.331,0.5661 0.331,1.3239 0,0.7577 -0.331,1.3239 -0.3223,0.5661 -0.8971,0.871 -0.5661,0.2961 -1.289,0.2961 -0.7229,0 -1.2977,-0.2961 -0.5748,-0.3048 -0.9058,-0.871 -0.3223,-0.5661 -0.3223,-1.3239 0,-0.7577 0.3223,-1.3239 0.331,-0.5661 0.9058,-0.8623 0.5748,-0.3048 1.2977,-0.3048zM50.0442,3.9879q-0.4268,0 -0.7229,0.3135 -0.2874,0.3048 -0.2874,0.8884 0,0.5836 0.2874,0.8884 0.2961,0.3048 0.7229,0.3048 0.4268,0 0.7142,-0.3048 0.2874,-0.3048 0.2874,-0.8884 0,-0.5836 -0.2874,-0.8884 -0.2874,-0.3135 -0.7142,-0.3135z\"/>\n</vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/drawable/bouncer_logo_light_background.xml",
    "content": "<vector android:autoMirrored=\"true\" android:height=\"29dp\"\n    android:viewportHeight=\"7.672917\" android:viewportWidth=\"52.916664\"\n    android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillAlpha=\"1\" android:fillColor=\"#1541f4\" android:pathData=\"m2.5003,2.6989q0.9406,0 1.5765,0.5226 0.6358,0.5139 0.8013,1.4197H3.3016Q3.232,4.3276 3.0142,4.1534 2.7965,3.9792 2.4655,3.9792q-0.3919,0 -0.6445,0.3135 -0.2526,0.3048 -0.2526,0.8971 0,0.5923 0.2526,0.9058 0.2526,0.3048 0.6445,0.3048 0.331,0 0.5487,-0.1742Q3.232,6.0521 3.3016,5.7386H4.8781Q4.7126,6.6444 4.0768,7.167 3.441,7.6809 2.5003,7.6809q-0.7142,0 -1.2716,-0.2961Q0.68,7.0799 0.3665,6.5138 0.0529,5.9476 0.0529,5.1899q0,-0.7665 0.3048,-1.3239Q0.6713,3.2999 1.2287,3.0037 1.7861,2.6989 2.5003,2.6989Z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#1859e8\" android:pathData=\"m7.4991,2.6724q0.5574,0 0.9581,0.2526 0.4006,0.2526 0.5923,0.6881V2.7334H10.5301V7.5934H9.0494V6.7137Q8.8578,7.1492 8.4572,7.4018 8.0565,7.6544 7.4991,7.6544q-0.601,0 -1.08,-0.2961 -0.4703,-0.3048 -0.749,-0.871 -0.27,-0.5661 -0.27,-1.3239 0,-0.7665 0.27,-1.3239Q5.9488,3.2734 6.4191,2.9773 6.8981,2.6724 7.4991,2.6724ZM7.9781,3.9789q-0.479,0 -0.7752,0.3135 -0.2874,0.3135 -0.2874,0.871 0,0.5574 0.2874,0.871 0.2961,0.3135 0.7752,0.3135 0.4703,0 0.7665,-0.3223 0.3048,-0.3223 0.3048,-0.8623 0,-0.5487 -0.3048,-0.8623Q8.4485,3.9789 7.9781,3.9789Z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#1b6ede\" android:pathData=\"M13.0183,3.6744Q13.297,3.2302 13.7325,2.9689 14.1767,2.7076 14.6906,2.7076v1.5939h-0.4268q-0.5923,0 -0.9232,0.2526Q13.0183,4.8066 13.0183,5.4076V7.6199H11.529V2.7599h1.4894z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#1d81d5\" android:pathData=\"m17.188,2.6989q0.5574,0 0.9581,0.2526 0.4006,0.2526 0.5835,0.6881v-2.4648h1.4894L20.219,7.6199L18.7296,7.6199L18.7296,6.7402q-0.1829,0.4355 -0.5835,0.6881 -0.4006,0.2526 -0.9581,0.2526 -0.601,0 -1.08,-0.2961 -0.4703,-0.3048 -0.749,-0.871 -0.27,-0.5661 -0.27,-1.3239 0,-0.7665 0.27,-1.3239 0.2787,-0.5661 0.749,-0.8623 0.479,-0.3048 1.08,-0.3048zM17.667,4.0054q-0.479,0 -0.7752,0.3135 -0.2874,0.3135 -0.2874,0.871 0,0.5574 0.2874,0.871 0.2961,0.3135 0.7752,0.3135 0.4703,0 0.7665,-0.3223 0.3048,-0.3223 0.3048,-0.8623 0,-0.5487 -0.3048,-0.8623 -0.2961,-0.3223 -0.7665,-0.3223z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#2096cb\" android:pathData=\"m22.9859,2.6989q0.9232,0 1.4719,0.4616 0.5574,0.4616 0.6968,1.2194H23.7611Q23.7001,4.0837 23.4824,3.9183 23.2733,3.7441 22.9511,3.7441q-0.2526,0 -0.3832,0.1132 -0.1306,0.1045 -0.1306,0.3048 0,0.2265 0.2352,0.3397 0.2439,0.1132 0.7577,0.2265 0.5574,0.1306 0.9145,0.27 0.3571,0.1306 0.6184,0.4355 0.2613,0.3048 0.2613,0.8187 0,0.4181 -0.2265,0.7403 -0.2265,0.3223 -0.6532,0.5052 -0.4268,0.1829 -1.0103,0.1829 -0.9842,0 -1.5765,-0.4355Q21.1656,6.8099 21.035,5.9999h1.4371q0.0348,0.3135 0.2613,0.479 0.2352,0.1655 0.601,0.1655 0.2526,0 0.3832,-0.1132 0.1306,-0.1219 0.1306,-0.3135 0,-0.2526 -0.2439,-0.3571Q23.3692,5.7473 22.8292,5.6254 22.2892,5.5121 21.9408,5.3815 21.5924,5.2508 21.3398,4.9634 21.0872,4.6673 21.0872,4.1621q0,-0.6532 0.4965,-1.0539 0.4965,-0.4094 1.4023,-0.4094z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#23abc1\" android:pathData=\"m28.2836,2.6989q0.9406,0 1.5765,0.5226 0.6358,0.5139 0.8013,1.4197H29.0849Q29.0152,4.3276 28.7975,4.1534 28.5797,3.9792 28.2488,3.9792q-0.3919,0 -0.6445,0.3135 -0.2526,0.3048 -0.2526,0.8971 0,0.5923 0.2526,0.9058 0.2526,0.3048 0.6445,0.3048 0.331,0 0.5487,-0.1742 0.2177,-0.1742 0.2874,-0.4877h1.5765q-0.1655,0.9058 -0.8013,1.4284 -0.6358,0.5139 -1.5765,0.5139 -0.7142,0 -1.2716,-0.2961 -0.5487,-0.3048 -0.8623,-0.871 -0.3135,-0.5661 -0.3135,-1.3239 0,-0.7665 0.3048,-1.3239 0.3135,-0.5661 0.871,-0.8623 0.5574,-0.3048 1.2716,-0.3048z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#26c2b6\" android:pathData=\"m33.2824,2.6989q0.5574,0 0.9581,0.2526 0.4006,0.2526 0.5923,0.6881L34.8327,2.7599L36.3134,2.7599L36.3134,7.6199L34.8327,7.6199L34.8327,6.7402q-0.1916,0.4355 -0.5923,0.6881 -0.4006,0.2526 -0.9581,0.2526 -0.601,0 -1.08,-0.2961Q31.732,7.0799 31.4533,6.5138 31.1833,5.9476 31.1833,5.1899q0,-0.7665 0.27,-1.3239Q31.732,3.2999 32.2024,3.0037 32.6814,2.6989 33.2824,2.6989ZM33.7614,4.0054q-0.479,0 -0.7752,0.3135 -0.2874,0.3135 -0.2874,0.871 0,0.5574 0.2874,0.871 0.2961,0.3135 0.7752,0.3135 0.4703,0 0.7665,-0.3223 0.3048,-0.3223 0.3048,-0.8623 0,-0.5487 -0.3048,-0.8623 -0.2961,-0.3223 -0.7665,-0.3223z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#2ad9ab\" android:pathData=\"m40.3955,2.7076q0.8361,0 1.3239,0.5574 0.4965,0.5574 0.4965,1.5329V7.6199H40.7264V4.9721q0,-0.4877 -0.2613,-0.7577 -0.2526,-0.2787 -0.6794,-0.2787 -0.4529,0 -0.7229,0.2874 -0.2613,0.2874 -0.2613,0.8187v2.5781h-1.4894V2.7599h1.4894v0.8884q0.209,-0.4355 0.6184,-0.6881 0.4181,-0.2526 0.9755,-0.2526z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#2deaa4\" android:pathData=\"M44.4953,6.1305V7.6199H42.9537V6.1305Z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#2ef4a0\" android:pathData=\"m45.1155,1.3924q0,-0.3397 0.2439,-0.5574 0.2439,-0.2265 0.6445,-0.2265 0.4006,0 0.6445,0.2265 0.2439,0.2177 0.2439,0.5574 0,0.331 -0.2439,0.5574 -0.2439,0.2177 -0.6445,0.2177 -0.4006,0 -0.6445,-0.2177 -0.2439,-0.2265 -0.2439,-0.5574zM46.7442,2.7599L46.7442,7.6199L45.2549,7.6199L45.2549,2.7599Z\"/>\n    <path android:fillAlpha=\"1\" android:fillColor=\"#30fa9e\" android:pathData=\"m50.0442,2.6989q0.7229,0 1.289,0.3048 0.5748,0.2961 0.8971,0.8623 0.331,0.5661 0.331,1.3239 0,0.7577 -0.331,1.3239 -0.3223,0.5661 -0.8971,0.871 -0.5661,0.2961 -1.289,0.2961 -0.7229,0 -1.2977,-0.2961 -0.5748,-0.3048 -0.9058,-0.871 -0.3223,-0.5661 -0.3223,-1.3239 0,-0.7577 0.3223,-1.3239 0.331,-0.5661 0.9058,-0.8623 0.5748,-0.3048 1.2977,-0.3048zM50.0442,3.9879q-0.4268,0 -0.7229,0.3135 -0.2874,0.3048 -0.2874,0.8884 0,0.5836 0.2874,0.8884 0.2961,0.3048 0.7229,0.3048 0.4268,0 0.7142,-0.3048 0.2874,-0.3048 0.2874,-0.8884 0,-0.5836 -0.2874,-0.8884 -0.2874,-0.3135 -0.7142,-0.3135z\"/>\n</vector>\n"
  },
  {
    "path": "scan-ui/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"bouncerCardPanColor\" description=\"The text color of the card number displayed over the scan window\">@android:color/white</color>\n    <color name=\"bouncerCardPanOutlineColor\" description=\"The outline color of the card number displayed over the scan window\">@android:color/black</color>\n\n    <color name=\"bouncerCardNameColor\" description=\"The text color of the cardholder name displayed over the scan window\">@android:color/white</color>\n    <color name=\"bouncerCardNameOutlineColor\" description=\"The outline color of the cardholder name displayed over the scan window\">@android:color/black</color>\n\n    <color name=\"bouncerCardExpiryColor\" description=\"The text color of the card expiry displayed over the scan window\">@android:color/white</color>\n    <color name=\"bouncerCardExpiryOutlineColor\" description=\"The outline color of the card expiry displayed over the scan window\">@android:color/black</color>\n\n    <color name=\"bouncerInstructionsColorDark\" description=\"The color of the instructions text above the scan window when the background is a dark color\">@android:color/white</color>\n    <color name=\"bouncerInstructionsColorLight\" description=\"The color of the instructions text above the scan window when the background is a light color\">@android:color/black</color>\n    <color name=\"bouncerSecurityColorDark\" description=\"The color of the security notification shown below the scan window when the background is a dark color\">@android:color/white</color>\n    <color name=\"bouncerSecurityColorLight\" description=\"The color of the security notification shown below the scan window when the background is a light color\">@android:color/black</color>\n\n    <color name=\"bouncerNotFoundBackground\" description=\"The background color of the scan window when no card is visible\">#DD222222</color>\n    <color name=\"bouncerFoundBackground\" description=\"The background color of the scan window when a card has been found and the scan is started\">#DD222222</color>\n    <color name=\"bouncerCorrectBackground\" description=\"The background color of the scan window when a card has been found and the scan is completed\">#DD222222</color>\n    <color name=\"bouncerWrongBackground\" description=\"The background color of the scan window when a card has been found that does not match the required card\">#DD222222</color>\n\n    <color name=\"bouncerNotFoundOutline\" description=\"The outline color of the scan window when no card is visible\">#FFFFFF</color>\n    <color name=\"bouncerFoundOutline\" description=\"The outline color of the scan window when a card has been found and the scan is started\">#1E90FF</color>\n    <color name=\"bouncerCorrectOutline\" description=\"The outline color of the scan window when the scan has completed\">#2ED573</color>\n    <color name=\"bouncerWrongOutline\" description=\"The outline color of the scan window when a card has been found that does not match the required card\">#FF2222</color>\n\n    <color name=\"bouncerCloseButtonDarkColor\" description=\"The color of the close button when the background is a dark color\">#FFFFFF</color>\n    <color name=\"bouncerFlashButtonDarkColor\" description=\"The color of the torch button when the background is a dark color\">#FFFFFF</color>\n    <color name=\"bouncerCameraSwapButtonDarkColor\" description=\"The color of the swap camera button when the background is a dark color\">#FFFFFF</color>\n    <color name=\"bouncerCloseButtonLightColor\" description=\"The color of the close button when the background is a light color\">#000000</color>\n    <color name=\"bouncerFlashButtonLightColor\" description=\"The color of the torch button when the background is a light color\">#000000</color>\n    <color name=\"bouncerCameraSwapButtonLightColor\" description=\"The color of the swap camera button when the background is a light color\">#000000</color>\n\n    <color name=\"bouncerDebugHighConfidence\" description=\"When using debug mode, the color of boxes to draw around high confidence text\">#00FF00</color>\n    <color name=\"bouncerDebugMediumConfidence\" description=\"When using debug mode, the color of boxes to draw around medium confidence text\">#DDDD00</color>\n    <color name=\"bouncerDebugLowConfidence\" description=\"When using debug mode, the color of boxes to draw around low confidence text\">#FF0000</color>\n</resources>\n"
  },
  {
    "path": "scan-ui/src/main/res/values/dimensions.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <item name=\"bouncerViewFinderMargin\" format=\"float\" type=\"dimen\" description=\"The minimum distance between the edge of the screen and the view finder window, in percentage of the screen\">0.08333333333</item>\n    <item name=\"bouncerViewFinderVerticalBias\" format=\"float\" type=\"dimen\" description=\"The vertical position of the view finder. Lower numbers shift the view finder towards the top of the screen\">0.5</item>\n    <item name=\"bouncerViewFinderHorizontalBias\" format=\"float\" type=\"dimen\" description=\"The horizontal position of the view finder. Lower numbers shift the view finder towards the left of the screen\">0.5</item>\n\n    <dimen name=\"bouncerCardDetailsMargin\" description=\"The minimum vertical and horizontal distance between the card details (number, expiry, name) and the border of the view finder window\">12dp</dimen>\n\n    <dimen name=\"bouncerInstructionsMargin\" description=\"The minimum amount of space surrounding the instructions text above the view finder window\">16dp</dimen>\n    <dimen name=\"bouncerSecurityMargin\" description=\"The minimum amount of space surrounding the security notification text below the view finder window\">16dp</dimen>\n    <dimen name=\"bouncerSecurityIconMargin\" description=\"The distance between the security notification lock icon and the security notification text\">4dp</dimen>\n\n    <item name=\"bouncerNotFoundOutlineWidth\" type=\"integer\" description=\"The width of the border around the view finder window when no card is present\">1</item>\n    <item name=\"bouncerFoundOutlineWidth\" type=\"integer\" description=\"The width of the border around the view finder window when a card has been found and the scan has started\">5</item>\n    <item name=\"bouncerCorrectOutlineWidth\" type=\"integer\" description=\"The width of the border around the view finder window when the scan completes\">10</item>\n    <item name=\"bouncerWrongOutlineWidth\" type=\"integer\" description=\"The width of the border around the view finder window when a card has been found but does not match the required card\">10</item>\n\n    <dimen name=\"bouncerPanTextSize\" description=\"The size of the card number text displayed in the middle of the view finder window\">20sp</dimen>\n    <item name=\"bouncerPanStrokeSize\" format=\"float\" type=\"dimen\" description=\"The size of the outline of the card number text displayed in the middle of the view finder window\">2.5</item>\n\n    <dimen name=\"bouncerNameTextSize\" description=\"The size of the cardholder name text displayed in the middle of the view finder window\">16sp</dimen>\n    <item name=\"bouncerNameStrokeSize\" format=\"float\" type=\"dimen\" description=\"The size of the outline of the cardholder name text displayed in the middle of the view finder window\">2.5</item>\n\n    <dimen name=\"bouncerExpiryTextSize\" description=\"The size of the expiry text displayed in the middle of the view finder window\">16sp</dimen>\n    <item name=\"bouncerExpiryStrokeSize\" format=\"float\" type=\"dimen\" description=\"The size of the outline of the expiry text displayed in the middle of the view finder window\">2.5</item>\n\n    <dimen name=\"bouncerInstructionsTextSize\" description=\"The size of the instructions text above the view finder window\">22sp</dimen>\n    <dimen name=\"bouncerSecurityTextSize\" description=\"The size of the security text below the view finder window\">14sp</dimen>\n\n    <dimen name=\"bouncerLogoMargin\" description=\"The minimum amount of space surrounding the logo\">16dp</dimen>\n    <item name=\"bouncerIconStrokeWidth\" type=\"integer\" description=\"The width of the lines of the close and torch buttons\">2</item>\n\n    <dimen name=\"bouncerDebugWindowWidth\" description=\"The width of the debug window\">192dp</dimen>\n    <dimen name=\"bouncerButtonMargin\" description=\"The amount of margin to place around the buttons\">0dp</dimen>\n</resources>\n"
  },
  {
    "path": "scan-ui/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"bouncer_card_scan_instructions\" description=\"Text displayed above the scan window, instructing the user how to scan card\">Scan Your Card</string>\n    <string name=\"bouncer_card_scan_security\" description=\"Security notification displayed below the scan window\">Your card info is secure</string>\n    <string name=\"bouncer_scanned_wrong_card\" description=\"Text displayed above the scan window when the wrong card is scanned\">Card doesn\\'t match</string>\n\n    <string name=\"bouncer_camera_permission_denied_message\" description=\"Message shown to the user when camera permission is denied\">Please allow camera access to scan your card</string>\n    <string name=\"bouncer_camera_permission_denied_ok\" description=\"Affirmative button shown as part of the permission denied dialog\">OK</string>\n    <string name=\"bouncer_camera_permission_denied_cancel\" description=\"Negative button shown as part of the permission denied dialog\">Cancel</string>\n\n    <string name=\"bouncer_error_camera_title\" description=\"Title of the dialog shown to the user when an error using the camera occurs\">Camera Problem</string>\n    <string name=\"bouncer_error_camera_open\" description=\"Message of the dialog shown to the user when an error starting the camera occurs\">The camera failed to turn on</string>\n    <string name=\"bouncer_error_camera_access\" description=\"Message of the dialog shown to the user when an error accessing the camera occurs\">Permission was denied when turning on the camera</string>\n    <string name=\"bouncer_error_camera_unsupported\" description=\"Message of the dialog shown to the user when the camera is not supported\">This device does not support the required camera features</string>\n    <string name=\"bouncer_error_camera_acknowledge_button\" description=\"Affirmative button shown as part of the camera error dialog\">Close</string>\n\n    <string name=\"bouncer_api_key_invalid_title\" description=\"Title of the dialog shown to the user when an invalid API key is specified\">Network Problem</string>\n    <string name=\"bouncer_api_key_invalid_message\" description=\"Message of the dialog shown to the user when an invalid API key is specified\">Sorry, this API key is not valid.</string>\n    <string name=\"bouncer_api_key_invalid_ok\" description=\"Affirmative button shown as part of the invalid API key dialog\">OK</string>\n\n    <string name=\"bouncer_cardscan_logo\" description=\"Accessibility description of the CardScan.io logo\">cardscan.io logo</string>\n    <string name=\"bouncer_security_description\" description=\"Accessibility description of the security notification lock icon\">lock icon</string>\n    <string name=\"bouncer_preview_description\" description=\"Accessibility description of the camera preview window\">card scanning window</string>\n    <string name=\"bouncer_debug_description\" description=\"Accessibility description of the debug preview window\">card debug window</string>\n    <string name=\"bouncer_close_button_description\" description=\"Text used to close the scan window\">Close</string>\n    <string name=\"bouncer_torch_button_description\" description=\"Text used to turn on the camera torch\">Torch</string>\n    <string name=\"bouncer_swap_camera_button_description\" description=\"Text used to swap the camera\">Swap Camera</string>\n    <string name=\"bouncer_card_view_finder_description\" description=\"Accessibility description of the view finder window\">card view finder</string>\n</resources>\n"
  },
  {
    "path": "scan-ui/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"Theme.AppCompat.Light.NoActionBar.FullScreen\" parent=\"@style/Theme.AppCompat.Light.NoActionBar\">\n        <item name=\"android:windowNoTitle\">true</item>\n        <item name=\"android:windowActionBar\">false</item>\n        <item name=\"android:windowFullscreen\">true</item>\n        <item name=\"android:windowContentOverlay\">@null</item>\n    </style>\n\n    <!-- Base application theme. -->\n    <style name=\"BouncerDefaultTheme\" parent=\"Theme.AppCompat.Light.NoActionBar.FullScreen\">\n        <item name=\"background\">@color/bouncerNotFoundBackground</item>\n\n        <item name=\"bouncerNotFoundBackgroundColor\">@color/bouncerNotFoundBackground</item>\n        <item name=\"bouncerNotFoundOutlineColor\">@color/bouncerNotFoundOutline</item>\n        <item name=\"bouncerNotFoundOutlineWidth\">@integer/bouncerNotFoundOutlineWidth</item>\n\n        <item name=\"bouncerFoundBackgroundColor\">@color/bouncerFoundBackground</item>\n        <item name=\"bouncerFoundOutlineColor\">@color/bouncerFoundOutline</item>\n        <item name=\"bouncerFoundOutlineWidth\">@integer/bouncerFoundOutlineWidth</item>\n\n        <item name=\"bouncerWrongBackgroundColor\">@color/bouncerWrongBackground</item>\n        <item name=\"bouncerWrongOutlineColor\">@color/bouncerWrongOutline</item>\n        <item name=\"bouncerWrongOutlineWidth\">@integer/bouncerWrongOutlineWidth</item>\n\n        <item name=\"bouncerCorrectBackgroundColor\">@color/bouncerCorrectBackground</item>\n        <item name=\"bouncerCorrectOutlineColor\">@color/bouncerCorrectOutline</item>\n        <item name=\"bouncerCorrectOutlineWidth\">@integer/bouncerCorrectOutlineWidth</item>\n\n        <item name=\"android:windowTranslucentStatus\">true</item>\n    </style>\n\n    <attr name=\"bouncerCardPanColor\" format=\"color\" />\n    <attr name=\"bouncerCardExpiryColor\" format=\"color\" />\n\n    <attr name=\"bouncerNotFoundBackgroundColor\" format=\"color\" />\n    <attr name=\"bouncerNotFoundOutlineColor\" format=\"color\" />\n    <attr name=\"bouncerNotFoundOutlineWidth\" format=\"dimension\" />\n\n    <attr name=\"bouncerFoundBackgroundColor\" format=\"color\" />\n    <attr name=\"bouncerFoundOutlineColor\" format=\"color\" />\n    <attr name=\"bouncerFoundOutlineWidth\" format=\"dimension\" />\n\n    <attr name=\"bouncerWrongBackgroundColor\" format=\"color\" />\n    <attr name=\"bouncerWrongOutlineColor\" format=\"color\" />\n    <attr name=\"bouncerWrongOutlineWidth\" format=\"dimension\" />\n\n    <attr name=\"bouncerCorrectBackgroundColor\" format=\"color\" />\n    <attr name=\"bouncerCorrectOutlineColor\" format=\"color\" />\n    <attr name=\"bouncerCorrectOutlineWidth\" format=\"dimension\" />\n\n    <declare-styleable name=\"ViewFinderBackground\">\n        <attr name=\"backgroundColor\" format=\"color\" />\n    </declare-styleable>\n\n</resources>\n"
  },
  {
    "path": "scan-ui/src/test/java/com/getbouncer/scan/ui/ExampleUnitTest.kt",
    "content": "package com.getbouncer.scan.ui\n\nimport org.junit.Test\nimport kotlin.test.assertEquals\n\nclass ExampleUnitTest {\n\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}\n"
  },
  {
    "path": "settings/checkstyle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE module PUBLIC\n    \"-//Puppy Crawl//DTD Check Configuration 1.3//EN\"\n    \"https://checkstyle.org/dtds/configuration_1_3.dtd\">\n\n<!-- This is a checkstyle configuration file. For descriptions of\nwhat the following rules do, please see the checkstyle configuration\npage at http://checkstyle.sourceforge.net/config.html -->\n\n<module name=\"Checker\">\n    <module name=\"SuppressWarningsFilter\" />\n\n    <module name=\"FileTabCharacter\">\n        <!-- Checks that there are no tab characters in the file.\n        -->\n    </module>\n\n    <module name=\"NewlineAtEndOfFile\">\n        <property name=\"lineSeparator\" value=\"lf\"/>\n    </module>\n    <!-- Checks that files are not allowed to be more than 1500 lines.-->\n    <module name=\"FileLength\">\n        <property name=\"max\" value=\"1500\"/>\n        <property name=\"severity\" value=\"warning\"/>\n    </module>\n\n\n    <module name=\"RegexpSingleline\">\n        <!-- Checks that FIXME is not used in comments.  TODO is preferred.\n        -->\n        <property name=\"format\" value=\"((//.*)|(\\*.*))FIXME\" />\n        <property name=\"message\" value='TODO is preferred to FIXME.  e.g. \"TODO(johndoe): Refactor when v2 is released.\"' />\n    </module>\n\n    <module name=\"RegexpSingleline\">\n        <!-- Checks that TODOs are named.  (Actually, just that they are followed\n             by an open paren.)\n        -->\n        <property name=\"format\" value=\"((//.*)|(\\*.*))TODO[^(]\" />\n        <property name=\"message\" value='All TODOs should be named.  e.g. \"TODO(johndoe): Refactor when v2 is released.\"' />\n    </module>\n\n    <module name=\"LineLength\">\n        <property name=\"fileExtensions\" value=\"java,kt\" />\n\n        <!-- Checks if a line is too long. -->\n        <property name=\"max\" value=\"${com.puppycrawl.tools.checkstyle.checks.sizes.LineLength.max}\" default=\"100\"/>\n        <property name=\"severity\" value=\"error\"/>\n\n        <!--\n          The default ignore pattern exempts the following elements:\n            - import statements\n            - long URLs inside comments\n        -->\n\n        <property name=\"ignorePattern\"\n            value=\"${com.puppycrawl.tools.checkstyle.checks.sizes.LineLength.ignorePattern}\"\n            default=\"^(package .*;\\s*)|(import .*;\\s*)|( *(\\*|//).*https?://.*)$\"/>\n    </module>\n\n    <!-- All Java AST specific tests live under TreeWalker module. -->\n    <module name=\"TreeWalker\">\n        <module name=\"SuppressWarningsHolder\" />\n\n        <!--\n        IMPORT CHECKS\n        -->\n        <module name=\"RedundantImport\">\n            <!-- Checks for redundant import statements. -->\n            <property name=\"severity\" value=\"error\"/>\n        </module>\n\n        <module name=\"ImportOrder\">\n            <property name=\"groups\" value=\"android,androidx,com,java,javax,org\"/>\n            <property name=\"ordered\" value=\"true\"/>\n            <property name=\"separated\" value=\"true\"/>\n            <property name=\"option\" value=\"bottom\"/>\n            <property name=\"sortStaticImportsAlphabetically\" value=\"true\"/>\n        </module>\n\n        <!--\n        JAVADOC CHECKS\n        -->\n\n        <!-- Checks for Javadoc comments.                     -->\n        <!-- See http://checkstyle.sf.net/config_javadoc.html -->\n        <module name=\"JavadocMethod\">\n            <property name=\"scope\" value=\"protected\"/>\n            <property name=\"severity\" value=\"warning\"/>\n            <property name=\"allowMissingParamTags\" value=\"true\"/>\n            <property name=\"allowMissingReturnTag\" value=\"true\"/>\n<!--            <property name=\"allowMissingThrowsTags\" value=\"true\"/>-->\n<!--            <property name=\"allowThrowsTagsForSubclasses\" value=\"true\"/>-->\n<!--            <property name=\"allowUndeclaredRTE\" value=\"true\"/>-->\n        </module>\n\n        <module name=\"MissingJavadocMethod\" />\n\n        <!--\n        NAMING CHECKS\n        -->\n        <!-- See http://checkstyle.sf.net/config_naming.html -->\n        <module name=\"ConstantName\"/>\n        <module name=\"LocalFinalVariableName\"/>\n        <module name=\"LocalVariableName\"/>\n        <!--<module name=\"MemberName\">-->\n        <!--<property name=\"format\" value=\"^m[A-Z][a-zA-Z0-9]*$\"/>-->\n        <!--</module>-->\n        <module name=\"MethodName\"/>\n        <module name=\"ParameterName\"/>\n        <module name=\"StaticVariableName\"/>\n        <module name=\"TypeName\"/>\n\n        <!--\n        CURLY CHECKS\n        -->\n        <module name=\"LeftCurly\">\n            <!-- Checks for placement of the left curly brace ('{'). -->\n            <property name=\"severity\" value=\"warning\"/>\n        </module>\n\n        <module name=\"RightCurly\">\n            <!-- Checks right curlies on CATCH, ELSE, and TRY blocks are on\n            the same line. e.g., the following example is fine:\n            <pre>\n              if {\n                ...\n              } else\n            </pre>\n            -->\n            <!-- This next example is not fine:\n            <pre>\n              if {\n                ...\n              }\n              else\n            </pre>\n            -->\n            <property name=\"option\" value=\"same\"/>\n            <property name=\"severity\" value=\"warning\"/>\n        </module>\n\n\n        <!--\n        MODIFIERS CHECKS\n        -->\n\n        <module name=\"ModifierOrder\">\n            <!-- Warn if modifier order is inconsistent with JLS3 8.1.1, 8.3.1, and\n                 8.4.3.  The prescribed order is:\n                 public, protected, private, abstract, static, final, transient, volatile,\n                 synchronized, native, strictfp\n              -->\n        </module>\n        <module name=\"RedundantModifier\"/>\n\n\n        <!--\n        WHITESPACE CHECKS\n        -->\n\n        <module name=\"WhitespaceAround\">\n            <!-- Checks that various tokens are surrounded by whitespace.\n                 This includes most binary operators and keywords followed\n                 by regular or curly braces.\n            -->\n            <property name=\"tokens\" value=\"ASSIGN, BAND, BAND_ASSIGN, BOR,\n        BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN,\n        EQUAL, GE, GT, LAND, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE,\n        LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN,\n        LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS,\n        MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION,\n        SL, SL_ASSIGN, SR_ASSIGN, STAR, STAR_ASSIGN\"/>\n            <property name=\"severity\" value=\"error\"/>\n        </module>\n\n        <module name=\"WhitespaceAfter\">\n            <!-- Checks that commas, semicolons and typecasts are followed by\n                 whitespace.\n            -->\n            <property name=\"tokens\" value=\"COMMA, SEMI, TYPECAST\"/>\n        </module>\n\n        <module name=\"NoWhitespaceAfter\">\n            <!-- Checks that there is no whitespace after various unary operators.\n                 Linebreaks are allowed.\n            -->\n            <property name=\"tokens\" value=\"BNOT, DEC, DOT, INC, LNOT, UNARY_MINUS,\n        UNARY_PLUS\"/>\n            <property name=\"allowLineBreaks\" value=\"true\"/>\n            <property name=\"severity\" value=\"error\"/>\n        </module>\n\n        <module name=\"NoWhitespaceBefore\">\n            <!-- Checks that there is no whitespace before various unary operators.\n                 Linebreaks are allowed.\n            -->\n            <property name=\"tokens\" value=\"SEMI, DOT, POST_DEC, POST_INC\"/>\n            <property name=\"allowLineBreaks\" value=\"true\"/>\n            <property name=\"severity\" value=\"error\"/>\n        </module>\n\n        <module name=\"ParenPad\">\n            <!-- Checks that there is no whitespace before close parens or after\n                 open parens.\n            -->\n            <property name=\"severity\" value=\"warning\"/>\n        </module>\n        <module name=\"GenericWhitespace\"/>\n\n        <module name=\"EmptyLineSeparator\">\n            <property name=\"allowNoEmptyLineBetweenFields\" value=\"true\"/>\n            <property name=\"allowMultipleEmptyLinesInsideClassMembers\" value=\"false\"/>\n        </module>\n\n        <!-- Checks for common coding problems               -->\n        <!-- See http://checkstyle.sf.net/config_coding.html -->\n        <module name=\"CovariantEquals\"/>\n        <module name=\"EmptyStatement\"/>\n        <module name=\"EqualsAvoidNull\"/>\n        <module name=\"EqualsHashCode\"/>\n        <module name=\"IllegalInstantiation\"/>\n        <module name=\"IllegalCatch\"/>\n        <module name=\"MissingSwitchDefault\"/>\n        <module name=\"SimplifyBooleanExpression\"/>\n        <module name=\"SimplifyBooleanReturn\"/>\n        <module name=\"StringLiteralEquality\"/>\n\n    </module>\n</module>\n"
  },
  {
    "path": "settings.gradle",
    "content": "rootProject.name='cardscan-android'\ninclude ':cardscan-demo'\ninclude ':cardscan-ui'\ninclude ':scan-camera'\ninclude ':scan-camera2'\ninclude ':scan-camerax'\ninclude ':scan-framework'\ninclude ':scan-payment'\ninclude ':scan-payment-full'\ninclude ':scan-payment-minimal'\ninclude ':scan-ui'\ninclude ':tensorflow-lite'\ninclude ':tensorflow-lite-arm-only'\n"
  },
  {
    "path": "tensorflow-lite/.gitignore",
    "content": "/build\n*.asc\n"
  },
  {
    "path": "tensorflow-lite/build.gradle",
    "content": "configurations.maybeCreate(\"default\")\nartifacts.add(\"default\", file('tensorflow-lite-all-models.aar'))\n\napply from: \"deploy.gradle\"\n"
  },
  {
    "path": "tensorflow-lite/deploy.gradle",
    "content": "apply plugin: 'maven-publish'\napply plugin: 'signing'\n\next[\"signing.keyId\"] = ''\next[\"signing.password\"] = ''\next[\"signing.secretKeyRingFile\"] = ''\n\next[\"ossrhUsername\"] = ''\next[\"ossrhPassword\"] = ''\next[\"sonatypeStagingProfileId\"] = ''\n\next {\n\n    libraryDescription = 'This library provides a custom implementation of TFLite for Bouncer products.'\n\n    siteUrl = 'https://getbouncer.com'\n\n    scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'\n    scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'\n    scmUrl = 'https://github.com/getbouncer/cardscan-android'\n\n    licenseName = 'bouncer-free-1'\n    licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'\n\n    developerId = 'getbouncer'\n    developerName = 'Bouncer Technologies'\n    developerEmail = 'bouncer-support@stripe.com'\n\n    publishGroupId = 'com.getbouncer'\n    publishArtifactId = 'tensorflow-lite'\n    publishVersion = version\n}\n\ngroup = publishGroupId\nversion = publishVersion\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    Properties p = new Properties()\n    new FileInputStream(secretPropsFile).withCloseable { is ->\n        p.load(is)\n    }\n    p.each { name, value ->\n        ext[name] = value\n    }\n} else {\n    ext[\"signing.keyId\"] = System.getenv('SIGNING_KEY_ID')\n    ext[\"signing.password\"] = System.getenv('SIGNING_PASSWORD')\n    ext[\"signing.secretKeyRingFile\"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')\n\n    ext[\"ossrhUsername\"] = System.getenv('OSSRH_USERNAME')\n    ext[\"ossrhPassword\"] = System.getenv('OSSRH_PASSWORD')\n\n    ext[\"sonatypeStagingProfileId\"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')\n}\n\npublishing {\n    publications {\n        maven(MavenPublication) {\n            groupId publishGroupId\n            artifactId publishArtifactId\n            version publishVersion\n            artifact \"$buildDir/../tensorflow-lite-all-models.aar\"\n\n            pom {\n                name = publishArtifactId\n                description = libraryDescription\n                url = siteUrl\n                licenses {\n                    license {\n                        name = licenseName\n                        url = licenseUrl\n                    }\n                }\n                developers {\n                    developer {\n                        id = developerId\n                        name = developerName\n                        email = developerEmail\n                    }\n                }\n                scm {\n                    connection = scmConnection\n                    developerConnection = scmDeveloperConnection\n                    url = scmUrl\n                }\n            }\n        }\n    }\n\n    // The repository to publish to, Sonatype/MavenCentral\n    repositories {\n        maven {\n            // This is an arbitrary name, you may also use \"mavencentral\" or\n            // any other name that's descriptive for you\n            name = \"sonatype\"\n            url = \"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/\"\n            credentials {\n                username ossrhUsername\n                password ossrhPassword\n            }\n        }\n    }\n}\n\nsigning {\n    sign publishing.publications\n}\n"
  },
  {
    "path": "tensorflow-lite-arm-only/.gitignore",
    "content": "/build\n*.asc\n"
  },
  {
    "path": "tensorflow-lite-arm-only/build.gradle",
    "content": "configurations.maybeCreate(\"default\")\nartifacts.add(\"default\", file('tensorflow-lite-all-models-arm-only.aar'))\n\napply from: \"deploy.gradle\"\n"
  },
  {
    "path": "tensorflow-lite-arm-only/deploy.gradle",
    "content": "apply plugin: 'maven-publish'\napply plugin: 'signing'\n\next[\"signing.keyId\"] = ''\next[\"signing.password\"] = ''\next[\"signing.secretKeyRingFile\"] = ''\n\next[\"ossrhUsername\"] = ''\next[\"ossrhPassword\"] = ''\next[\"sonatypeStagingProfileId\"] = ''\n\next {\n\n    libraryDescription = 'This library provides a custom implementation of TFLite for Bouncer products.'\n\n    siteUrl = 'https://getbouncer.com'\n\n    scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'\n    scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'\n    scmUrl = 'https://github.com/getbouncer/cardscan-android'\n\n    licenseName = 'bouncer-free-1'\n    licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'\n\n    developerId = 'getbouncer'\n    developerName = 'Bouncer Technologies'\n    developerEmail = 'bouncer-support@stripe.com'\n\n    publishGroupId = 'com.getbouncer'\n    publishArtifactId = 'tensorflow-lite-arm-only'\n    publishVersion = version\n}\n\ngroup = publishGroupId\nversion = publishVersion\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    Properties p = new Properties()\n    new FileInputStream(secretPropsFile).withCloseable { is ->\n        p.load(is)\n    }\n    p.each { name, value ->\n        ext[name] = value\n    }\n} else {\n    ext[\"signing.keyId\"] = System.getenv('SIGNING_KEY_ID')\n    ext[\"signing.password\"] = System.getenv('SIGNING_PASSWORD')\n    ext[\"signing.secretKeyRingFile\"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')\n\n    ext[\"ossrhUsername\"] = System.getenv('OSSRH_USERNAME')\n    ext[\"ossrhPassword\"] = System.getenv('OSSRH_PASSWORD')\n\n    ext[\"sonatypeStagingProfileId\"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')\n}\n\npublishing {\n    publications {\n        maven(MavenPublication) {\n            groupId publishGroupId\n            artifactId publishArtifactId\n            version publishVersion\n            artifact \"$buildDir/../tensorflow-lite-all-models-arm-only.aar\"\n\n            pom {\n                name = publishArtifactId\n                description = libraryDescription\n                url = siteUrl\n                licenses {\n                    license {\n                        name = licenseName\n                        url = licenseUrl\n                    }\n                }\n                developers {\n                    developer {\n                        id = developerId\n                        name = developerName\n                        email = developerEmail\n                    }\n                }\n                scm {\n                    connection = scmConnection\n                    developerConnection = scmDeveloperConnection\n                    url = scmUrl\n                }\n            }\n        }\n    }\n\n    // The repository to publish to, Sonatype/MavenCentral\n    repositories {\n        maven {\n            // This is an arbitrary name, you may also use \"mavencentral\" or\n            // any other name that's descriptive for you\n            name = \"sonatype\"\n            url = \"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/\"\n            credentials {\n                username ossrhUsername\n                password ossrhPassword\n            }\n        }\n    }\n}\n\nsigning {\n    sign publishing.publications\n}\n"
  }
]