Repository: getbouncer/cardscan-android
Branch: master
Commit: bf490afe479e
Files: 272
Total size: 1.1 MB
Directory structure:
gitextract_79p3qz5m/
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── android_test.yml
│ ├── lint.yml
│ ├── release.yml
│ └── unit_test.yml
├── .github_changelog_generator
├── .gitignore
├── CHANGELOG.md
├── CODEOWNERS
├── Contributor License Agreement
├── HISTORY.md
├── LICENSE
├── README.md
├── build.gradle
├── cardscan-demo/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── getbouncer/
│ │ └── cardscan/
│ │ └── demo/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── getbouncer/
│ │ │ └── cardscan/
│ │ │ └── demo/
│ │ │ ├── LaunchActivity.java
│ │ │ └── SingleActivityDemo.java
│ │ └── res/
│ │ ├── drawable/
│ │ │ └── ic_launcher_background.xml
│ │ ├── drawable-v24/
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── layout/
│ │ │ ├── activity_launch.xml
│ │ │ └── activity_single_demo.xml
│ │ ├── mipmap-anydpi-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ └── values/
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test/
│ └── java/
│ └── com/
│ └── getbouncer/
│ └── cardscan/
│ └── demo/
│ └── ExampleUnitTest.kt
├── cardscan-ui/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle
│ ├── consumer-rules.pro
│ ├── deploy.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── getbouncer/
│ │ └── cardscan/
│ │ └── ui/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── getbouncer/
│ │ │ └── cardscan/
│ │ │ └── ui/
│ │ │ ├── CardScanActivity.kt
│ │ │ ├── CardScanBaseActivity.kt
│ │ │ ├── CardScanFlow.kt
│ │ │ ├── CardScanSheet.kt
│ │ │ ├── analyzer/
│ │ │ │ ├── CompletionLoopAnalyzer.kt
│ │ │ │ └── MainLoopAnalyzer.kt
│ │ │ ├── exception/
│ │ │ │ ├── StripeNetworkException.kt
│ │ │ │ └── UnknownScanException.kt
│ │ │ └── result/
│ │ │ ├── CompletionLoopAggregator.kt
│ │ │ ├── MainLoopAggregator.kt
│ │ │ └── MainLoopStateMachine.kt
│ │ └── res/
│ │ └── values/
│ │ ├── colors.xml
│ │ ├── dimensions.xml
│ │ └── strings.xml
│ └── test/
│ └── java/
│ └── com/
│ └── getbouncer/
│ └── cardscan/
│ └── ui/
│ └── result/
│ └── MainLoopStateMachineTest.kt
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── scan-camera/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle
│ ├── consumer-rules.pro
│ ├── deploy.gradle
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── com/
│ └── getbouncer/
│ └── scan/
│ └── camera/
│ ├── Camera1Adapter.kt
│ ├── CameraAdapter.kt
│ └── CameraSelector.kt
├── scan-camera2/
│ ├── .gitignore
│ ├── build.gradle
│ ├── deploy.gradle
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── getbouncer/
│ │ └── scan/
│ │ └── camera/
│ │ └── extension/
│ │ └── UtilInstrumentationTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── getbouncer/
│ │ └── scan/
│ │ └── camera/
│ │ └── extension/
│ │ ├── CameraAdapterImpl.kt
│ │ ├── CameraDetails.kt
│ │ └── Util.kt
│ └── test/
│ └── java/
│ └── com/
│ └── getbouncer/
│ └── scan/
│ └── camera/
│ └── extension/
│ └── UtilTest.kt
├── scan-camerax/
│ ├── .gitignore
│ ├── build.gradle
│ ├── deploy.gradle
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── com/
│ └── getbouncer/
│ └── scan/
│ └── camera/
│ └── extension/
│ ├── CameraAdapterImpl.kt
│ ├── Image.kt
│ └── Util.kt
├── scan-framework/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle
│ ├── consumer-rules.pro
│ ├── deploy.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ ├── assets/
│ │ │ └── sample_resource.tflite
│ │ └── java/
│ │ └── com/
│ │ └── getbouncer/
│ │ └── scan/
│ │ └── framework/
│ │ ├── FetcherTest.kt
│ │ ├── LoaderTest.kt
│ │ ├── StorageTest.kt
│ │ ├── api/
│ │ │ └── BouncerApiTest.kt
│ │ ├── image/
│ │ │ └── BitmapExtensionsTest.kt
│ │ ├── layout/
│ │ │ └── LayoutTest.kt
│ │ └── util/
│ │ └── AppDetailsTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── getbouncer/
│ │ └── scan/
│ │ └── framework/
│ │ ├── Analyzer.kt
│ │ ├── Config.kt
│ │ ├── Fetcher.kt
│ │ ├── Loader.kt
│ │ ├── Loop.kt
│ │ ├── MachineState.kt
│ │ ├── Result.kt
│ │ ├── Scan.kt
│ │ ├── Stat.kt
│ │ ├── Storage.kt
│ │ ├── TrackedImage.kt
│ │ ├── api/
│ │ │ ├── BouncerApi.kt
│ │ │ ├── Network.kt
│ │ │ ├── NetworkResult.kt
│ │ │ └── dto/
│ │ │ ├── AppInfo.kt
│ │ │ ├── BouncerErrorResponse.kt
│ │ │ ├── ClientDevice.kt
│ │ │ ├── ClientStats.kt
│ │ │ ├── ModelDetails.kt
│ │ │ ├── ModelSignedUrlResponse.kt
│ │ │ └── ValidateApiKeyResponse.kt
│ │ ├── exception/
│ │ │ ├── ImageTypeNotSupportedException.kt
│ │ │ └── InvalidBouncerApiKeyException.kt
│ │ ├── image/
│ │ │ ├── BitmapExtensions.kt
│ │ │ ├── ImageExtensions.kt
│ │ │ ├── MLImage.kt
│ │ │ ├── NV21Image.kt
│ │ │ └── YuvImageExtensions.kt
│ │ ├── interop/
│ │ │ ├── BlockingAnalyzer.kt
│ │ │ ├── BlockingResult.kt
│ │ │ └── JavaContinuation.kt
│ │ ├── ml/
│ │ │ ├── ModelVersionTracker.kt
│ │ │ ├── NonMaximumSuppression.kt
│ │ │ ├── TensorFlowLiteAnalyzer.kt
│ │ │ └── ssd/
│ │ │ ├── ClassifierScores.kt
│ │ │ ├── RectForm.kt
│ │ │ └── SizeAndCenter.kt
│ │ ├── time/
│ │ │ ├── Clock.kt
│ │ │ ├── Coroutine.kt
│ │ │ ├── Duration.kt
│ │ │ ├── Rate.kt
│ │ │ └── Timer.kt
│ │ └── util/
│ │ ├── AppDetails.kt
│ │ ├── ArrayExtensions.kt
│ │ ├── Device.kt
│ │ ├── File.kt
│ │ ├── FrameRateTracker.kt
│ │ ├── FrameSaver.kt
│ │ ├── ItemCounter.kt
│ │ ├── Layout.kt
│ │ ├── Memoize.kt
│ │ └── Retry.kt
│ └── test/
│ └── java/
│ └── com/
│ └── getbouncer/
│ └── scan/
│ └── framework/
│ ├── AnalyzerTest.kt
│ ├── LoopTest.kt
│ ├── interop/
│ │ ├── BlockingAnalyzerTest.java
│ │ └── BlockingResultTest.java
│ ├── time/
│ │ └── DurationTest.kt
│ └── util/
│ ├── ArrayExtensionsTest.kt
│ ├── FrameSaverTest.kt
│ ├── ItemCounterTest.kt
│ ├── MemoizeTest.kt
│ └── RetryTest.kt
├── scan-payment/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle
│ ├── consumer-rules.pro
│ ├── deploy.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── getbouncer/
│ │ └── scan/
│ │ └── payment/
│ │ ├── ImageTest.kt
│ │ ├── PaymentCardAndroidTest.kt
│ │ └── ml/
│ │ ├── CardDetectTest.kt
│ │ ├── ExpiryDetectTest.kt
│ │ ├── SSDOcrTest.kt
│ │ ├── TextDetectTest.kt
│ │ └── ssd/
│ │ └── SSDTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── getbouncer/
│ │ └── scan/
│ │ └── payment/
│ │ ├── FrameDetails.kt
│ │ ├── Image.kt
│ │ ├── ModelManager.kt
│ │ ├── TextDetectModelManager.kt
│ │ ├── analyzer/
│ │ │ └── NameAndExpiryAnalyzer.kt
│ │ ├── card/
│ │ │ ├── CardExpiry.kt
│ │ │ ├── CardIssuer.kt
│ │ │ ├── CardType.kt
│ │ │ ├── PanFormatter.kt
│ │ │ ├── PanValidator.kt
│ │ │ ├── PaymentCard.kt
│ │ │ ├── PaymentCardUtils.kt
│ │ │ └── RequiresMatchingCard.kt
│ │ └── ml/
│ │ ├── AlphabetDetect.kt
│ │ ├── AlphabetDetectModelManager.kt
│ │ ├── CardDetect.kt
│ │ ├── CardDetectModelManager.kt
│ │ ├── ExpiryDetect.kt
│ │ ├── ExpiryDetectModelManager.kt
│ │ ├── SSDOcr.kt
│ │ ├── SSDOcrModelManager.kt
│ │ ├── TextDetect.kt
│ │ ├── ssd/
│ │ │ ├── DetectionBox.kt
│ │ │ ├── OcrPriorsGenerator.kt
│ │ │ └── SSD.kt
│ │ └── yolo/
│ │ └── Yolo.kt
│ └── test/
│ └── java/
│ └── com/
│ └── getbouncer/
│ └── scan/
│ └── payment/
│ └── card/
│ └── PaymentCardTest.kt
├── scan-payment-full/
│ ├── .gitignore
│ ├── build.gradle
│ ├── deploy.gradle
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── getbouncer/
│ │ └── scan/
│ │ └── payment/
│ │ └── ml/
│ │ ├── CardDetectTest.kt
│ │ └── SSDOcrTest.kt
│ └── main/
│ ├── AndroidManifest.xml
│ └── assets/
│ ├── darknite_1_1_1_16.tflite
│ └── ux_0_5_23_16.tflite
├── scan-payment-minimal/
│ ├── .gitignore
│ ├── build.gradle
│ ├── deploy.gradle
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── getbouncer/
│ │ └── scan/
│ │ └── payment/
│ │ └── ml/
│ │ ├── CardDetectTest.kt
│ │ └── SSDOcrTest.kt
│ └── main/
│ ├── AndroidManifest.xml
│ └── assets/
│ ├── UX.0.25.106.8.tflite
│ └── mb2_brex_metal_synthetic_svhnextra_epoch_3_5_98_8.tflite
├── scan-ui/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle
│ ├── consumer-rules.pro
│ ├── deploy.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── getbouncer/
│ │ │ └── scan/
│ │ │ └── ui/
│ │ │ ├── DebugOverlayTest.kt
│ │ │ └── util/
│ │ │ └── ViewExtensionsTest.kt
│ │ └── res/
│ │ └── values/
│ │ └── colors.xml
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── getbouncer/
│ │ │ └── scan/
│ │ │ └── ui/
│ │ │ ├── DebugOverlay.kt
│ │ │ ├── ScanActivity.kt
│ │ │ ├── ScanFlow.kt
│ │ │ ├── SimpleScanActivity.kt
│ │ │ ├── ViewFinderBackground.kt
│ │ │ └── util/
│ │ │ └── ViewExtensions.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── bouncer_camera_swap_dark.xml
│ │ │ ├── bouncer_camera_swap_light.xml
│ │ │ ├── bouncer_card_background_correct.xml
│ │ │ ├── bouncer_card_background_found.xml
│ │ │ ├── bouncer_card_background_not_found.xml
│ │ │ ├── bouncer_card_background_wrong.xml
│ │ │ ├── bouncer_card_border_correct.xml
│ │ │ ├── bouncer_card_border_found.xml
│ │ │ ├── bouncer_card_border_found_long.xml
│ │ │ ├── bouncer_card_border_not_found.xml
│ │ │ ├── bouncer_card_border_wrong.xml
│ │ │ ├── bouncer_close_button_dark.xml
│ │ │ ├── bouncer_close_button_light.xml
│ │ │ ├── bouncer_flash_off_dark.xml
│ │ │ ├── bouncer_flash_off_light.xml
│ │ │ ├── bouncer_flash_on_dark.xml
│ │ │ ├── bouncer_flash_on_light.xml
│ │ │ ├── bouncer_lock_dark.xml
│ │ │ ├── bouncer_lock_light.xml
│ │ │ ├── bouncer_logo_dark_background.xml
│ │ │ └── bouncer_logo_light_background.xml
│ │ └── values/
│ │ ├── colors.xml
│ │ ├── dimensions.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test/
│ └── java/
│ └── com/
│ └── getbouncer/
│ └── scan/
│ └── ui/
│ └── ExampleUnitTest.kt
├── settings/
│ └── checkstyle.xml
├── settings.gradle
├── tensorflow-lite/
│ ├── .gitignore
│ ├── build.gradle
│ ├── deploy.gradle
│ └── tensorflow-lite-all-models.aar
└── tensorflow-lite-arm-only/
├── .gitignore
├── build.gradle
├── deploy.gradle
└── tensorflow-lite-all-models-arm-only.aar
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: gradle
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
ignore:
- dependency-name: androidx.camera:camera-lifecycle
versions:
- 1.1.0-alpha04
- dependency-name: androidx.camera:camera-camera2
versions:
- 1.1.0-alpha04
- dependency-name: androidx.camera:camera-view
versions:
- 1.0.0-alpha24
- dependency-name: androidx.camera:camera-core
versions:
- 1.1.0-alpha04
- dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-android
versions:
- 1.4.3-native-mt
- dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-core
versions:
- 1.4.3-native-mt
- dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-test
versions:
- 1.4.3-native-mt
- dependency-name: org.jetbrains.kotlin:kotlin-test
versions:
- 1.4.31
- dependency-name: org.jetbrains.kotlin.plugin.serialization
versions:
- 1.4.30
- 1.4.31
- dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin
versions:
- 1.4.30
- 1.4.31
================================================
FILE: .github/workflows/android_test.yml
================================================
name: Instrumentation Tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
instrumentation-test:
runs-on: macOS-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: test
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
script: ./gradlew connectedCheck
- name: upload-artifacts
uses: actions/upload-artifact@v2
if: failure()
with:
name: test-report
path: ${{ github.workspace }}/*/build/reports/
================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: lint
run: ./gradlew ktlint
- name: CheckStyle
run: ./gradlew checkJavaStyle
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
release:
types: [published]
jobs:
run_final_checks:
name: Run final checks
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
with:
ref: ${{ github.event.release.target_commitish }}
token: ${{secrets.SERVICE_PERSONAL_ACCESS_TOKEN}}
submodules: recursive
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: lint
run: ./gradlew ktlint
- name: CheckStyle
run: ./gradlew checkJavaStyle
- name: test
run: ./gradlew test
finalize_release:
needs: run_final_checks
name: Finalize Release
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
with:
ref: ${{ github.event.release.target_commitish }}
token: ${{secrets.SERVICE_PERSONAL_ACCESS_TOKEN}}
submodules: recursive
- name: get current tag
id: get_tag
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: update version
env:
TAG_VERSION: ${{ steps.get_tag.outputs.VERSION }}
run: |
truncate -s $(( $(stat -c "%s" gradle.properties) - $(tail -n 1 gradle.properties | wc -c) )) gradle.properties
echo "version=$TAG_VERSION" >> gradle.properties
cat gradle.properties
- name: generate changelog
uses: heinrichreimer/github-changelog-generator-action@v2.1.1
with:
user: "getbouncer"
project: "cardscan-android"
repo: "getbouncer/cardscan-android"
token: ${{ secrets.SERVICE_PERSONAL_ACCESS_TOKEN }}
sinceTag: "1.0.5151"
pullRequests: "true"
prWoLabels: "true"
issues: "true"
issuesWoLabels: "true"
author: "true"
base: "HISTORY.md"
unreleased: "true"
breakingLabels: "Versioning - BREAKING"
enhancementLabels: "Type - Enhancement, Type - Feature"
bugLabels: "Type - Fix, Bug - Fixed"
deprecatedLabels: "Type - Deprecated"
removedLabels: "Type - Removal"
securityLabels: "Security Fix"
excludeLabels: "Skip-Changelog"
- name: create commit
id: commit
uses: stefanzweifel/git-auto-commit-action@v4
with:
branch: ${{ github.event.release.target_commitish }}
commit_message: "Automatic changelog update"
file_pattern: "gradle.properties CHANGELOG.md"
publish:
needs: finalize_release
name: Publish to MavenCentral
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
with:
ref: ${{ github.event.release.target_commitish }}
token: ${{secrets.SERVICE_PERSONAL_ACCESS_TOKEN}}
submodules: recursive
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
# Base64 decodes and pipes the GPG key content into the secret file
- name: Prepare environment
env:
GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY }}
SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}
run: |
git fetch --unshallow
sudo bash -c "echo '$GPG_KEY_CONTENTS' | base64 -d > '$SIGNING_SECRET_KEY_RING_FILE'"
- name: Build release
run: ./gradlew assembleRelease
- name: Source jar and dokka
run: ./gradlew androidSourcesJar javadocJar
- name: Publish to MavenCentral
env:
OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}
SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }}
run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository
================================================
FILE: .github/workflows/unit_test.yml
================================================
name: Unit Tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: test
run: ./gradlew test
- name: upload-artifacts
uses: actions/upload-artifact@v2
if: failure()
with:
name: test-report
path: ${{ github.workspace }}/*/build/reports/
================================================
FILE: .github_changelog_generator
================================================
since-tag=2.0.0015
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/compiler.xml
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/encodings.xml
/.idea/gradle.xml
/.idea/misc.xml
/.idea/runConfigurations.xml
/.idea/vcs.xml
/.idea/dictionaries
/.idea/codeStyles/Project.xml
/.idea/codeStyles/codeStyleConfig.xml
/.idea/.name
/.idea/checkstyle-idea.xml
/.idea/jarRepositories.xml
.DS_Store
/build
/captures
.externalNativeBuild
langapiconfig.json
*.cxx
github_token
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## [2.2.0003](https://github.com/getbouncer/cardscan-android/tree/2.2.0003) (2022-06-15)
**Closed issues:**
- Crash when intent is null in parseResult callback [\#506](https://github.com/getbouncer/cardscan-android/issues/506)
**Merged pull requests:**
- Add deprecation notice to cardscan-android [\#523](https://github.com/getbouncer/cardscan-android/pull/523) ([awush-stripe](https://github.com/awush-stripe))
- Remove references to bouncer emails [\#508](https://github.com/getbouncer/cardscan-android/pull/508) ([awush-stripe](https://github.com/awush-stripe))
## [2.2.0002](https://github.com/getbouncer/cardscan-android/tree/2.2.0002) (2022-04-11)
**Closed issues:**
- failed to build in android studio bumblebee [\#494](https://github.com/getbouncer/cardscan-android/issues/494)
**Merged pull requests:**
- Prevent crash on null result intent [\#507](https://github.com/getbouncer/cardscan-android/pull/507) ([awush-stripe](https://github.com/awush-stripe))
## [2.2.0001](https://github.com/getbouncer/cardscan-android/tree/2.2.0001) (2022-03-28)
**Closed issues:**
- The new CardScanSheet API is not testable [\#493](https://github.com/getbouncer/cardscan-android/issues/493)
- The new CardScanSheet API is not testable [\#492](https://github.com/getbouncer/cardscan-android/issues/492)
- The new CardScanSheet API doesn't deliver result when starter activity is destroyed [\#491](https://github.com/getbouncer/cardscan-android/issues/491)
- Getbouncer console not accessible. [\#487](https://github.com/getbouncer/cardscan-android/issues/487)
**Merged pull requests:**
- 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))
- Make the activity result registry testable [\#498](https://github.com/getbouncer/cardscan-android/pull/498) ([awush-stripe](https://github.com/awush-stripe))
## [2.1.0023](https://github.com/getbouncer/cardscan-android/tree/2.1.0023) (2022-01-18)
## [2.1.0022](https://github.com/getbouncer/cardscan-android/tree/2.1.0022) (2022-01-17)
## [2.1.0021](https://github.com/getbouncer/cardscan-android/tree/2.1.0021) (2022-01-06)
**Merged pull requests:**
- Simplify the API interface for CardScan [\#482](https://github.com/getbouncer/cardscan-android/pull/482) ([awush-stripe](https://github.com/awush-stripe))
## [2.1.0020](https://github.com/getbouncer/cardscan-android/tree/2.1.0020) (2021-12-21)
**Merged pull requests:**
- Correctly report final stats [\#475](https://github.com/getbouncer/cardscan-android/pull/475) ([awush-stripe](https://github.com/awush-stripe))
## [2.1.0019](https://github.com/getbouncer/cardscan-android/tree/2.1.0019) (2021-12-09)
## [2.1.0018](https://github.com/getbouncer/cardscan-android/tree/2.1.0018) (2021-10-20)
## [2.1.0017](https://github.com/getbouncer/cardscan-android/tree/2.1.0017) (2021-10-20)
**Merged pull requests:**
- Revert model storage file name [\#461](https://github.com/getbouncer/cardscan-android/pull/461) ([awush-stripe](https://github.com/awush-stripe))
## [2.1.0016](https://github.com/getbouncer/cardscan-android/tree/2.1.0016) (2021-10-04)
## [2.1.0015](https://github.com/getbouncer/cardscan-android/tree/2.1.0015) (2021-10-04)
**Merged pull requests:**
- Make prepareScan more accessible by using a callback [\#458](https://github.com/getbouncer/cardscan-android/pull/458) ([awush-stripe](https://github.com/awush-stripe))
## [2.1.0014](https://github.com/getbouncer/cardscan-android/tree/2.1.0014) (2021-09-24)
**Merged pull requests:**
- Fix memory leak and double camera unbind [\#456](https://github.com/getbouncer/cardscan-android/pull/456) ([awush-stripe](https://github.com/awush-stripe))
## [2.1.0014-alpha02](https://github.com/getbouncer/cardscan-android/tree/2.1.0014-alpha02) (2021-09-23)
## [2.1.0014-alpha01](https://github.com/getbouncer/cardscan-android/tree/2.1.0014-alpha01) (2021-09-17)
## [2.1.0014-downgrade-core-ktx01](https://github.com/getbouncer/cardscan-android/tree/2.1.0014-downgrade-core-ktx01) (2021-09-17)
## [2.1.0013](https://github.com/getbouncer/cardscan-android/tree/2.1.0013) (2021-09-17)
**Closed issues:**
- java.lang.RuntimeException: getParameters failed \(empty parameters\) [\#448](https://github.com/getbouncer/cardscan-android/issues/448)
**Merged pull requests:**
- Add stat tracking fetcher [\#451](https://github.com/getbouncer/cardscan-android/pull/451) ([awush-stripe](https://github.com/awush-stripe))
## [2.1.0012](https://github.com/getbouncer/cardscan-android/tree/2.1.0012) (2021-09-15)
**Merged pull requests:**
- 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))
## [2.1.0011](https://github.com/getbouncer/cardscan-android/tree/2.1.0011) (2021-09-08)
**Merged pull requests:**
- Destory created renderscript types [\#447](https://github.com/getbouncer/cardscan-android/pull/447) ([awush-stripe](https://github.com/awush-stripe))
## [2.1.0010](https://github.com/getbouncer/cardscan-android/tree/2.1.0010) (2021-09-01)
**Merged pull requests:**
- Allow ranges of dependencies [\#442](https://github.com/getbouncer/cardscan-android/pull/442) ([awush-stripe](https://github.com/awush-stripe))
## [2.1.0009](https://github.com/getbouncer/cardscan-android/tree/2.1.0009) (2021-07-29)
**Closed issues:**
- "Cannot add a null child view to a ViewGroup" Exception on Camera1Adapter [\#435](https://github.com/getbouncer/cardscan-android/issues/435)
- CardScanActivity vs CardScanFlow warmUp/prepareScan [\#434](https://github.com/getbouncer/cardscan-android/issues/434)
- Are Tensorflow/lite vulnerabilities a concern? [\#427](https://github.com/getbouncer/cardscan-android/issues/427)
- Expiry date overlay doesn't show [\#406](https://github.com/getbouncer/cardscan-android/issues/406)
**Merged pull requests:**
- 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))
## [2.1.0008](https://github.com/getbouncer/cardscan-android/tree/2.1.0008) (2021-07-19)
**Merged pull requests:**
- Simplify onScanReady [\#426](https://github.com/getbouncer/cardscan-android/pull/426) ([awush-stripe](https://github.com/awush-stripe))
## [2.1.0007](https://github.com/getbouncer/cardscan-android/tree/2.1.0007) (2021-07-15)
**Merged pull requests:**
- Downgrade kotlin gradle plugin [\#422](https://github.com/getbouncer/cardscan-android/pull/422) ([awush-stripe](https://github.com/awush-stripe))
## [2.1.0006](https://github.com/getbouncer/cardscan-android/tree/2.1.0006) (2021-07-15)
**Merged pull requests:**
- Downgrade kotlin methods in loop [\#421](https://github.com/getbouncer/cardscan-android/pull/421) ([awush-stripe](https://github.com/awush-stripe))
- Downgrade kotlin libraries to 1.4.3 [\#420](https://github.com/getbouncer/cardscan-android/pull/420) ([awush-stripe](https://github.com/awush-stripe))
## [2.1.0005-ktx1.4.3](https://github.com/getbouncer/cardscan-android/tree/2.1.0005-ktx1.4.3) (2021-07-15)
## [2.1.0005](https://github.com/getbouncer/cardscan-android/tree/2.1.0005) (2021-07-14)
**Merged pull requests:**
- Update dependencies [\#419](https://github.com/getbouncer/cardscan-android/pull/419) ([awush-stripe](https://github.com/awush-stripe))
- Update coroutines [\#418](https://github.com/getbouncer/cardscan-android/pull/418) ([awush-stripe](https://github.com/awush-stripe))
- 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))
- 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))
- 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))
- 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))
- 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))
- 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))
- 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))
- 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))
- 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))
- 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))
- 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))
## [2.1.0004](https://github.com/getbouncer/cardscan-android/tree/2.1.0004) (2021-07-12)
**Closed issues:**
- Missing dependency on Maven Central updating to 2.1.0003 [\#407](https://github.com/getbouncer/cardscan-android/issues/407)
**Merged pull requests:**
- 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))
- 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))
- Update card images used for testing [\#396](https://github.com/getbouncer/cardscan-android/pull/396) ([awush-stripe](https://github.com/awush-stripe))
## [2.1.0003](https://github.com/getbouncer/cardscan-android/tree/2.1.0003) (2021-06-18)
**Merged pull requests:**
- Cycle beta versions faster [\#395](https://github.com/getbouncer/cardscan-android/pull/395) ([awush-stripe](https://github.com/awush-stripe))
- 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))
## [2.1.0002](https://github.com/getbouncer/cardscan-android/tree/2.1.0002) (2021-06-11)
**Closed issues:**
- Some card scans reversed [\#388](https://github.com/getbouncer/cardscan-android/issues/388)
**Merged pull requests:**
- Make scan ready checks JVM static [\#393](https://github.com/getbouncer/cardscan-android/pull/393) ([awush-stripe](https://github.com/awush-stripe))
## [2.1.0001](https://github.com/getbouncer/cardscan-android/tree/2.1.0001) (2021-06-11)
**Merged pull requests:**
- Add QR false positive detection check [\#392](https://github.com/getbouncer/cardscan-android/pull/392) ([awush-stripe](https://github.com/awush-stripe))
- 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))
- Default to download OCR / UX models [\#390](https://github.com/getbouncer/cardscan-android/pull/390) ([awush-stripe](https://github.com/awush-stripe))
- Upgrade dependencies [\#389](https://github.com/getbouncer/cardscan-android/pull/389) ([awush-stripe](https://github.com/awush-stripe))
## [2.0.0090](https://github.com/getbouncer/cardscan-android/tree/2.0.0090) (2021-05-24)
## [2.0.0089](https://github.com/getbouncer/cardscan-android/tree/2.0.0089) (2021-05-24)
**Merged pull requests:**
- Use button margin dimensions [\#381](https://github.com/getbouncer/cardscan-android/pull/381) ([awush-stripe](https://github.com/awush-stripe))
- 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))
- 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))
- 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))
- 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))
## [2.0.0088](https://github.com/getbouncer/cardscan-android/tree/2.0.0088) (2021-05-18)
**Merged pull requests:**
- Improve error handling around cache calculation [\#377](https://github.com/getbouncer/cardscan-android/pull/377) ([awushensky](https://github.com/awushensky))
## [2.0.0087](https://github.com/getbouncer/cardscan-android/tree/2.0.0087) (2021-05-13)
## [2.0.0086](https://github.com/getbouncer/cardscan-android/tree/2.0.0086) (2021-05-13)
**Merged pull requests:**
- Remove jcenter [\#370](https://github.com/getbouncer/cardscan-android/pull/370) ([awushensky](https://github.com/awushensky))
## [2.0.0085](https://github.com/getbouncer/cardscan-android/tree/2.0.0085) (2021-05-12)
**Closed issues:**
- Feature request: scan a vertical/portrait card [\#346](https://github.com/getbouncer/cardscan-android/issues/346)
- name and year extraction not working [\#336](https://github.com/getbouncer/cardscan-android/issues/336)
**Merged pull requests:**
- 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))
- 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))
- 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))
- Remove compile dependency on camerax [\#367](https://github.com/getbouncer/cardscan-android/pull/367) ([awushensky](https://github.com/awushensky))
- Support multiple cameras and add swap camera string [\#362](https://github.com/getbouncer/cardscan-android/pull/362) ([awushensky](https://github.com/awushensky))
- Upgrade to GitHub-native Dependabot [\#361](https://github.com/getbouncer/cardscan-android/pull/361) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- 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))
- 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))
- 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))
- 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))
- 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))
- 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))
- 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))
## [2.0.0084](https://github.com/getbouncer/cardscan-android/tree/2.0.0084) (2021-04-20)
**Closed issues:**
- Fatal exception due to wrong coroutine context [\#348](https://github.com/getbouncer/cardscan-android/issues/348)
**Merged pull requests:**
- Use camera2 APIs [\#347](https://github.com/getbouncer/cardscan-android/pull/347) ([awushensky](https://github.com/awushensky))
## [2.0.0083](https://github.com/getbouncer/cardscan-android/tree/2.0.0083) (2021-04-12)
**Merged pull requests:**
- Ensure camera error handler runs on main thread [\#349](https://github.com/getbouncer/cardscan-android/pull/349) ([awushensky](https://github.com/awushensky))
- 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))
## [2.0.0082](https://github.com/getbouncer/cardscan-android/tree/2.0.0082) (2021-03-30)
## [2.0.0081](https://github.com/getbouncer/cardscan-android/tree/2.0.0081) (2021-03-30)
**Merged pull requests:**
- Correctly name the artifact for scan-payment-base [\#345](https://github.com/getbouncer/cardscan-android/pull/345) ([awushensky](https://github.com/awushensky))
## [2.0.0080](https://github.com/getbouncer/cardscan-android/tree/2.0.0080) (2021-03-30)
**Merged pull requests:**
- Update kotlin versions [\#344](https://github.com/getbouncer/cardscan-android/pull/344) ([awushensky](https://github.com/awushensky))
- Update release names [\#339](https://github.com/getbouncer/cardscan-android/pull/339) ([awushensky](https://github.com/awushensky))
## [2.0.0079](https://github.com/getbouncer/cardscan-android/tree/2.0.0079) (2021-03-27)
**Merged pull requests:**
- Swap to maven central [\#338](https://github.com/getbouncer/cardscan-android/pull/338) ([awushensky](https://github.com/awushensky))
## [2.0.0078](https://github.com/getbouncer/cardscan-android/tree/2.0.0078) (2021-03-26)
**Closed issues:**
- Require specifying ML models through dependencies [\#332](https://github.com/getbouncer/cardscan-android/issues/332)
**Merged pull requests:**
- Fix name and expiry extraction [\#337](https://github.com/getbouncer/cardscan-android/pull/337) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- 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))
- Support minimal ML models [\#331](https://github.com/getbouncer/cardscan-android/pull/331) ([awushensky](https://github.com/awushensky))
- 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))
## [2.0.0077](https://github.com/getbouncer/cardscan-android/tree/2.0.0077) (2021-03-23)
**Merged pull requests:**
- Reduce sdk size [\#328](https://github.com/getbouncer/cardscan-android/pull/328) ([awushensky](https://github.com/awushensky))
## [2.0.0076](https://github.com/getbouncer/cardscan-android/tree/2.0.0076) (2021-03-09)
## [2.0.0075](https://github.com/getbouncer/cardscan-android/tree/2.0.0075) (2021-03-06)
**Merged pull requests:**
- Add version code [\#326](https://github.com/getbouncer/cardscan-android/pull/326) ([awushensky](https://github.com/awushensky))
- Upgrade gradle and kotlin [\#325](https://github.com/getbouncer/cardscan-android/pull/325) ([awushensky](https://github.com/awushensky))
## [2.0.0074](https://github.com/getbouncer/cardscan-android/tree/2.0.0074) (2021-02-18)
## [2.0.0073](https://github.com/getbouncer/cardscan-android/tree/2.0.0073) (2021-02-18)
**Closed issues:**
- java.lang.NoClassDefFoundError: Failed resolution of: Lorg/tensorflow/lite/Interpreter$Options in 2.0.0072 [\#319](https://github.com/getbouncer/cardscan-android/issues/319)
- Card scan crash on release [\#316](https://github.com/getbouncer/cardscan-android/issues/316)
**Merged pull requests:**
- 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))
- Allow configuring no model downloads [\#327](https://github.com/getbouncer/cardscan-android/pull/327) ([awushensky](https://github.com/awushensky))
- Make card number display optional [\#321](https://github.com/getbouncer/cardscan-android/pull/321) ([awushensky](https://github.com/awushensky))
- 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))
- Upgrade kotlin dependencies [\#318](https://github.com/getbouncer/cardscan-android/pull/318) ([awushensky](https://github.com/awushensky))
- 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))
## [2.0.0072](https://github.com/getbouncer/cardscan-android/tree/2.0.0072) (2021-02-04)
**Closed issues:**
- Crash on animation [\#299](https://github.com/getbouncer/cardscan-android/issues/299)
**Merged pull requests:**
- Fix crash on fade animations when minifying resources [\#317](https://github.com/getbouncer/cardscan-android/pull/317) ([awushensky](https://github.com/awushensky))
## [2.0.0071](https://github.com/getbouncer/cardscan-android/tree/2.0.0071) (2021-02-01)
**Merged pull requests:**
- Custom tensorflow lite library [\#312](https://github.com/getbouncer/cardscan-android/pull/312) ([awushensky](https://github.com/awushensky))
## [2.0.0070](https://github.com/getbouncer/cardscan-android/tree/2.0.0070) (2021-01-29)
**Merged pull requests:**
- Separate ocr ux models [\#311](https://github.com/getbouncer/cardscan-android/pull/311) ([awushensky](https://github.com/awushensky))
- Do not expire old models [\#310](https://github.com/getbouncer/cardscan-android/pull/310) ([awushensky](https://github.com/awushensky))
## [2.0.0069](https://github.com/getbouncer/cardscan-android/tree/2.0.0069) (2021-01-27)
**Merged pull requests:**
- Add cardscan local [\#309](https://github.com/getbouncer/cardscan-android/pull/309) ([awushensky](https://github.com/awushensky))
- 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))
## [2.0.0068](https://github.com/getbouncer/cardscan-android/tree/2.0.0068) (2021-01-13)
**Merged pull requests:**
- Relocate payment card [\#307](https://github.com/getbouncer/cardscan-android/pull/307) ([awushensky](https://github.com/awushensky))
## [2.0.0067](https://github.com/getbouncer/cardscan-android/tree/2.0.0067) (2021-01-11)
## [2.0.0066](https://github.com/getbouncer/cardscan-android/tree/2.0.0066) (2021-01-09)
## [2.0.0065](https://github.com/getbouncer/cardscan-android/tree/2.0.0065) (2021-01-09)
**Merged pull requests:**
- Fix memoization race condition [\#306](https://github.com/getbouncer/cardscan-android/pull/306) ([awushensky](https://github.com/awushensky))
## [2.0.0064](https://github.com/getbouncer/cardscan-android/tree/2.0.0064) (2021-01-09)
**Merged pull requests:**
- Allow optional fetchers [\#305](https://github.com/getbouncer/cardscan-android/pull/305) ([awushensky](https://github.com/awushensky))
## [2.0.0063](https://github.com/getbouncer/cardscan-android/tree/2.0.0063) (2021-01-04)
## [2.0.0062](https://github.com/getbouncer/cardscan-android/tree/2.0.0062) (2020-12-31)
## [2.0.0061](https://github.com/getbouncer/cardscan-android/tree/2.0.0061) (2020-12-31)
**Merged pull requests:**
- Improve analyzer performance [\#301](https://github.com/getbouncer/cardscan-android/pull/301) ([awushensky](https://github.com/awushensky))
## [2.0.0060](https://github.com/getbouncer/cardscan-android/tree/2.0.0060) (2020-12-22)
## [2.0.0059](https://github.com/getbouncer/cardscan-android/tree/2.0.0059) (2020-12-21)
**Merged pull requests:**
- Improve low-end performance [\#300](https://github.com/getbouncer/cardscan-android/pull/300) ([awushensky](https://github.com/awushensky))
- Update kotlin test [\#298](https://github.com/getbouncer/cardscan-android/pull/298) ([awushensky](https://github.com/awushensky))
- Make normalizeCardNumber public [\#297](https://github.com/getbouncer/cardscan-android/pull/297) ([awushensky](https://github.com/awushensky))
- 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))
## [2.0.0058](https://github.com/getbouncer/cardscan-android/tree/2.0.0058) (2020-12-15)
**Merged pull requests:**
- Fix clearing stats prematurely [\#295](https://github.com/getbouncer/cardscan-android/pull/295) ([awushensky](https://github.com/awushensky))
## [2.0.0057](https://github.com/getbouncer/cardscan-android/tree/2.0.0057) (2020-12-14)
**Closed issues:**
- Mention new introduced strings in changelog and docs. [\#291](https://github.com/getbouncer/cardscan-android/issues/291)
**Merged pull requests:**
- Use app context for network [\#294](https://github.com/getbouncer/cardscan-android/pull/294) ([awushensky](https://github.com/awushensky))
- Update serializer [\#293](https://github.com/getbouncer/cardscan-android/pull/293) ([awushensky](https://github.com/awushensky))
- Upgrade kotlin [\#292](https://github.com/getbouncer/cardscan-android/pull/292) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- 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))
- 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))
## [2.0.0056](https://github.com/getbouncer/cardscan-android/tree/2.0.0056) (2020-12-03)
**Closed issues:**
- Integration of the library shows this. [\#256](https://github.com/getbouncer/cardscan-android/issues/256)
**Merged pull requests:**
- Support expirys up to 100 years in the future [\#286](https://github.com/getbouncer/cardscan-android/pull/286) ([awushensky](https://github.com/awushensky))
- Support 3-digit CVC for amex [\#285](https://github.com/getbouncer/cardscan-android/pull/285) ([awushensky](https://github.com/awushensky))
- Upgrade dependencies [\#278](https://github.com/getbouncer/cardscan-android/pull/278) ([awushensky](https://github.com/awushensky))
## [2.0.0055](https://github.com/getbouncer/cardscan-android/tree/2.0.0055) (2020-11-25)
**Merged pull requests:**
- Support beta model opt-in [\#277](https://github.com/getbouncer/cardscan-android/pull/277) ([awushensky](https://github.com/awushensky))
## [2.0.0054](https://github.com/getbouncer/cardscan-android/tree/2.0.0054) (2020-11-19)
**Merged pull requests:**
- Use network stack for model downloads [\#270](https://github.com/getbouncer/cardscan-android/pull/270) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
## [2.0.0053](https://github.com/getbouncer/cardscan-android/tree/2.0.0053) (2020-11-13)
**Merged pull requests:**
- Make permissions requests make more sense [\#269](https://github.com/getbouncer/cardscan-android/pull/269) ([awushensky](https://github.com/awushensky))
## [2.0.0052](https://github.com/getbouncer/cardscan-android/tree/2.0.0052) (2020-11-13)
**Merged pull requests:**
- Reset stats at scan start [\#268](https://github.com/getbouncer/cardscan-android/pull/268) ([awushensky](https://github.com/awushensky))
## [2.0.0051](https://github.com/getbouncer/cardscan-android/tree/2.0.0051) (2020-11-10)
**Merged pull requests:**
- Do not use Date.toString due to crashes [\#266](https://github.com/getbouncer/cardscan-android/pull/266) ([awushensky](https://github.com/awushensky))
## [2.0.0050](https://github.com/getbouncer/cardscan-android/tree/2.0.0050) (2020-10-26)
**Merged pull requests:**
- Upgrade gradle to 4.1.0 [\#257](https://github.com/getbouncer/cardscan-android/pull/257) ([awushensky](https://github.com/awushensky))
- Send model hash with info request [\#250](https://github.com/getbouncer/cardscan-android/pull/250) ([awushensky](https://github.com/awushensky))
## [2.0.0049](https://github.com/getbouncer/cardscan-android/tree/2.0.0049) (2020-10-21)
**Merged pull requests:**
- Name and expiry in completion loop [\#255](https://github.com/getbouncer/cardscan-android/pull/255) ([awushensky](https://github.com/awushensky))
## [2.0.0048](https://github.com/getbouncer/cardscan-android/tree/2.0.0048) (2020-10-19)
**Merged pull requests:**
- Use latest version in loader [\#254](https://github.com/getbouncer/cardscan-android/pull/254) ([awushensky](https://github.com/awushensky))
- Fix stats concurrent modification [\#253](https://github.com/getbouncer/cardscan-android/pull/253) ([awushensky](https://github.com/awushensky))
- Enable delay analysis [\#252](https://github.com/getbouncer/cardscan-android/pull/252) ([awushensky](https://github.com/awushensky))
## [2.0.0047](https://github.com/getbouncer/cardscan-android/tree/2.0.0047) (2020-10-15)
**Merged pull requests:**
- Remove autofocus feature requirement [\#251](https://github.com/getbouncer/cardscan-android/pull/251) ([awushensky](https://github.com/awushensky))
## [1.0.5155](https://github.com/getbouncer/cardscan-android/tree/1.0.5155) (2020-10-15)
## [2.0.0046](https://github.com/getbouncer/cardscan-android/tree/2.0.0046) (2020-10-14)
## [2.0.0045](https://github.com/getbouncer/cardscan-android/tree/2.0.0045) (2020-10-12)
**Merged pull requests:**
- Upgrade gradle [\#248](https://github.com/getbouncer/cardscan-android/pull/248) ([awushensky](https://github.com/awushensky))
- Force model download if no cache [\#247](https://github.com/getbouncer/cardscan-android/pull/247) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
## [1.0.5153](https://github.com/getbouncer/cardscan-android/tree/1.0.5153) (2020-10-09)
## [1.0.5154](https://github.com/getbouncer/cardscan-android/tree/1.0.5154) (2020-10-09)
**Merged pull requests:**
- Fix zoom image [\#245](https://github.com/getbouncer/cardscan-android/pull/245) ([awushensky](https://github.com/awushensky))
- Support zoomed model [\#236](https://github.com/getbouncer/cardscan-android/pull/236) ([awushensky](https://github.com/awushensky))
## [2.0.0044](https://github.com/getbouncer/cardscan-android/tree/2.0.0044) (2020-10-09)
**Merged pull requests:**
- Parallelize model downloads [\#244](https://github.com/getbouncer/cardscan-android/pull/244) ([awushensky](https://github.com/awushensky))
- Clean up some OCR [\#243](https://github.com/getbouncer/cardscan-android/pull/243) ([awushensky](https://github.com/awushensky))
- Improve fetcher logging [\#242](https://github.com/getbouncer/cardscan-android/pull/242) ([awushensky](https://github.com/awushensky))
- Target android 30 [\#240](https://github.com/getbouncer/cardscan-android/pull/240) ([awushensky](https://github.com/awushensky))
## [2.0.0043](https://github.com/getbouncer/cardscan-android/tree/2.0.0043) (2020-10-06)
**Merged pull requests:**
- Fix autofocus more [\#239](https://github.com/getbouncer/cardscan-android/pull/239) ([awushensky](https://github.com/awushensky))
- Remove camera1 autofocus repeater [\#238](https://github.com/getbouncer/cardscan-android/pull/238) ([awushensky](https://github.com/awushensky))
- Clean up image utils [\#237](https://github.com/getbouncer/cardscan-android/pull/237) ([awushensky](https://github.com/awushensky))
## [1.0.5152](https://github.com/getbouncer/cardscan-android/tree/1.0.5152) (2020-10-05)
**Merged pull requests:**
- Upgrade ocr model [\#235](https://github.com/getbouncer/cardscan-android/pull/235) ([awushensky](https://github.com/awushensky))
## [2.0.0042](https://github.com/getbouncer/cardscan-android/tree/2.0.0042) (2020-10-02)
**Merged pull requests:**
- Support model upgrade delay [\#234](https://github.com/getbouncer/cardscan-android/pull/234) ([awushensky](https://github.com/awushensky))
- 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))
## [2.0.0041](https://github.com/getbouncer/cardscan-android/tree/2.0.0041) (2020-10-02)
## [2.0.0040](https://github.com/getbouncer/cardscan-android/tree/2.0.0040) (2020-10-01)
**Merged pull requests:**
- Fix cancelation memory leak [\#232](https://github.com/getbouncer/cardscan-android/pull/232) ([awushensky](https://github.com/awushensky))
- Fix frame saver memory leak [\#231](https://github.com/getbouncer/cardscan-android/pull/231) ([awushensky](https://github.com/awushensky))
## [2.0.0039](https://github.com/getbouncer/cardscan-android/tree/2.0.0039) (2020-09-30)
**Merged pull requests:**
- Tune the name & expiry extraction analyzers [\#230](https://github.com/getbouncer/cardscan-android/pull/230) ([awushensky](https://github.com/awushensky))
- Split main loop [\#229](https://github.com/getbouncer/cardscan-android/pull/229) ([awushensky](https://github.com/awushensky))
- Calculate and adapt to device speed [\#228](https://github.com/getbouncer/cardscan-android/pull/228) ([awushensky](https://github.com/awushensky))
- Prevent duplicate final results [\#227](https://github.com/getbouncer/cardscan-android/pull/227) ([awushensky](https://github.com/awushensky))
## [2.0.0038](https://github.com/getbouncer/cardscan-android/tree/2.0.0038) (2020-09-25)
**Merged pull requests:**
- Clean up resources [\#226](https://github.com/getbouncer/cardscan-android/pull/226) ([awushensky](https://github.com/awushensky))
## [2.0.0037](https://github.com/getbouncer/cardscan-android/tree/2.0.0037) (2020-09-25)
**Closed issues:**
- Demo app crashed [\#223](https://github.com/getbouncer/cardscan-android/issues/223)
## [2.0.0036](https://github.com/getbouncer/cardscan-android/tree/2.0.0036) (2020-09-24)
**Merged pull requests:**
- Clean up single activity demo [\#225](https://github.com/getbouncer/cardscan-android/pull/225) ([awushensky](https://github.com/awushensky))
## [2.0.0035](https://github.com/getbouncer/cardscan-android/tree/2.0.0035) (2020-09-24)
**Merged pull requests:**
- Fix crash on single activity demo [\#224](https://github.com/getbouncer/cardscan-android/pull/224) ([awushensky](https://github.com/awushensky))
- Clean up UI changes [\#222](https://github.com/getbouncer/cardscan-android/pull/222) ([awushensky](https://github.com/awushensky))
- Add java continuation support [\#221](https://github.com/getbouncer/cardscan-android/pull/221) ([awushensky](https://github.com/awushensky))
- Create programmatic UI [\#217](https://github.com/getbouncer/cardscan-android/pull/217) ([awushensky](https://github.com/awushensky))
## [2.0.0034](https://github.com/getbouncer/cardscan-android/tree/2.0.0034) (2020-09-22)
**Merged pull requests:**
- Allow manual camera pause [\#216](https://github.com/getbouncer/cardscan-android/pull/216) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- 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))
## [2.0.0033](https://github.com/getbouncer/cardscan-android/tree/2.0.0033) (2020-09-17)
## [2.0.0032](https://github.com/getbouncer/cardscan-android/tree/2.0.0032) (2020-09-11)
**Merged pull requests:**
- Fix a camera crash on revvl2 devices [\#212](https://github.com/getbouncer/cardscan-android/pull/212) ([awushensky](https://github.com/awushensky))
- Support better camera autofocus [\#211](https://github.com/getbouncer/cardscan-android/pull/211) ([awushensky](https://github.com/awushensky))
- Update state machine tests [\#210](https://github.com/getbouncer/cardscan-android/pull/210) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
## [2.0.0031](https://github.com/getbouncer/cardscan-android/tree/2.0.0031) (2020-09-11)
**Merged pull requests:**
- fixes the bitmap test [\#207](https://github.com/getbouncer/cardscan-android/pull/207) ([dxaen](https://github.com/dxaen))
## [2.0.0030](https://github.com/getbouncer/cardscan-android/tree/2.0.0030) (2020-09-08)
**Closed issues:**
- Analyzer failure with DexGuard enabled [\#202](https://github.com/getbouncer/cardscan-android/issues/202)
**Merged pull requests:**
- Add unit tests for cardscan state machine [\#206](https://github.com/getbouncer/cardscan-android/pull/206) ([awushensky](https://github.com/awushensky))
- Quick read support for android [\#203](https://github.com/getbouncer/cardscan-android/pull/203) ([dxaen](https://github.com/dxaen))
## [2.0.0029](https://github.com/getbouncer/cardscan-android/tree/2.0.0029) (2020-09-08)
**Merged pull requests:**
- Add proguard rules for tensorflow [\#205](https://github.com/getbouncer/cardscan-android/pull/205) ([awushensky](https://github.com/awushensky))
- Standardize expiry to strings [\#204](https://github.com/getbouncer/cardscan-android/pull/204) ([awushensky](https://github.com/awushensky))
## [2.0.0028](https://github.com/getbouncer/cardscan-android/tree/2.0.0028) (2020-09-03)
**Merged pull requests:**
- Open up the UI [\#201](https://github.com/getbouncer/cardscan-android/pull/201) ([awushensky](https://github.com/awushensky))
- Add card payment type data [\#198](https://github.com/getbouncer/cardscan-android/pull/198) ([awushensky](https://github.com/awushensky))
## [2.0.0027](https://github.com/getbouncer/cardscan-android/tree/2.0.0027) (2020-09-01)
**Merged pull requests:**
- Clean up state machine [\#199](https://github.com/getbouncer/cardscan-android/pull/199) ([awushensky](https://github.com/awushensky))
## [2.0.0026](https://github.com/getbouncer/cardscan-android/tree/2.0.0026) (2020-08-27)
**Merged pull requests:**
- 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))
- 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))
- 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))
- 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))
- 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))
## [2.0.0025](https://github.com/getbouncer/cardscan-android/tree/2.0.0025) (2020-08-24)
**Merged pull requests:**
- 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))
## [2.0.0024](https://github.com/getbouncer/cardscan-android/tree/2.0.0024) (2020-08-21)
## [2.0.0023](https://github.com/getbouncer/cardscan-android/tree/2.0.0023) (2020-08-21)
**Merged pull requests:**
- Enable minification on cardscan demo [\#191](https://github.com/getbouncer/cardscan-android/pull/191) ([awushensky](https://github.com/awushensky))
- Relocate ktlint [\#190](https://github.com/getbouncer/cardscan-android/pull/190) ([awushensky](https://github.com/awushensky))
- Fix accessibility descriptions [\#189](https://github.com/getbouncer/cardscan-android/pull/189) ([awushensky](https://github.com/awushensky))
## [2.0.0022](https://github.com/getbouncer/cardscan-android/tree/2.0.0022) (2020-08-18)
## [2.0.0021](https://github.com/getbouncer/cardscan-android/tree/2.0.0021) (2020-08-18)
**Closed issues:**
- How to scan other types of cards? [\#150](https://github.com/getbouncer/cardscan-android/issues/150)
**Merged pull requests:**
- Chang custom card issuer [\#185](https://github.com/getbouncer/cardscan-android/pull/185) ([smkuhne](https://github.com/smkuhne))
- 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))
- Add custom pans [\#183](https://github.com/getbouncer/cardscan-android/pull/183) ([smkuhne](https://github.com/smkuhne))
- Update dependencies [\#182](https://github.com/getbouncer/cardscan-android/pull/182) ([awushensky](https://github.com/awushensky))
- Local rules [\#181](https://github.com/getbouncer/cardscan-android/pull/181) ([awushensky](https://github.com/awushensky))
## [2.0.0020](https://github.com/getbouncer/cardscan-android/tree/2.0.0020) (2020-08-13)
**Closed issues:**
- Crash on android 5 Lenovo [\#89](https://github.com/getbouncer/cardscan-android/issues/89)
**Merged pull requests:**
- Add aspect ratio method [\#173](https://github.com/getbouncer/cardscan-android/pull/173) ([smkuhne](https://github.com/smkuhne))
- Prevent crash on bad model download [\#172](https://github.com/getbouncer/cardscan-android/pull/172) ([awushensky](https://github.com/awushensky))
## [2.0.0019](https://github.com/getbouncer/cardscan-android/tree/2.0.0019) (2020-08-12)
**Merged pull requests:**
- Fix display bug [\#171](https://github.com/getbouncer/cardscan-android/pull/171) ([awushensky](https://github.com/awushensky))
- Support extracting iin and last4 from utils [\#170](https://github.com/getbouncer/cardscan-android/pull/170) ([awushensky](https://github.com/awushensky))
- Add check result [\#169](https://github.com/getbouncer/cardscan-android/pull/169) ([awushensky](https://github.com/awushensky))
- 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))
- Add zoomOffset method [\#167](https://github.com/getbouncer/cardscan-android/pull/167) ([smkuhne](https://github.com/smkuhne))
- Use key without permissions for test [\#166](https://github.com/getbouncer/cardscan-android/pull/166) ([awushensky](https://github.com/awushensky))
- Update expiry timeout, handle new permissions [\#165](https://github.com/getbouncer/cardscan-android/pull/165) ([xsl](https://github.com/xsl))
- Add documentation [\#164](https://github.com/getbouncer/cardscan-android/pull/164) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
## [2.0.0018](https://github.com/getbouncer/cardscan-android/tree/2.0.0018) (2020-07-28)
**Closed issues:**
- name and expiry date [\#82](https://github.com/getbouncer/cardscan-android/issues/82)
**Merged pull requests:**
- Clean up counters [\#162](https://github.com/getbouncer/cardscan-android/pull/162) ([awushensky](https://github.com/awushensky))
- Clean up state machine logic [\#161](https://github.com/getbouncer/cardscan-android/pull/161) ([awushensky](https://github.com/awushensky))
- Clean up state machine [\#160](https://github.com/getbouncer/cardscan-android/pull/160) ([awushensky](https://github.com/awushensky))
- Main loop state machine [\#158](https://github.com/getbouncer/cardscan-android/pull/158) ([awushensky](https://github.com/awushensky))
## [2.0.0017](https://github.com/getbouncer/cardscan-android/tree/2.0.0017) (2020-07-22)
**Merged pull requests:**
- Update api key validation check [\#156](https://github.com/getbouncer/cardscan-android/pull/156) ([awushensky](https://github.com/awushensky))
- Update changelog [\#155](https://github.com/getbouncer/cardscan-android/pull/155) ([smkuhne](https://github.com/smkuhne))
## [2.0.0016](https://github.com/getbouncer/cardscan-android/tree/2.0.0016) (2020-07-20)
**Merged pull requests:**
- Bump version to 2.0.0016 [\#154](https://github.com/getbouncer/cardscan-android/pull/154) ([smkuhne](https://github.com/smkuhne))
- Add readmes [\#153](https://github.com/getbouncer/cardscan-android/pull/153) ([awushensky](https://github.com/awushensky))
- Rename demo module [\#152](https://github.com/getbouncer/cardscan-android/pull/152) ([awushensky](https://github.com/awushensky))
## [2.0.0015](https://github.com/getbouncer/cardscan-android/tree/2.0.0015) (2020-07-18)
**Merged pull requests:**
- Restructure 2.0 [\#151](https://github.com/getbouncer/cardscan-android/pull/151) ([awushensky](https://github.com/awushensky))
## [2.0.0014](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0014) (2020-07-14)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0013...2.0.0014)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0013...2.0.0014)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0013...2.0.0014)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0013...2.0.0014)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0013...2.0.0014)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0013...2.0.0014)
**Merged pull requests:**
- Image fragmentation [\#53](https://github.com/getbouncer/scan-framework-android/pull/53) ([smkuhne](https://github.com/smkuhne))
- Standardize models [\#35](https://github.com/getbouncer/scan-payment-android/pull/35) ([awushensky](https://github.com/awushensky))
- Image fragmentation [\#33](https://github.com/getbouncer/scan-payment-android/pull/33) ([smkuhne](https://github.com/smkuhne))
- Allow extending uploadstats [\#30](https://github.com/getbouncer/scan-ui-android/pull/30) ([awushensky](https://github.com/awushensky))
- Image fragmentation [\#28](https://github.com/getbouncer/scan-ui-android/pull/28) ([smkuhne](https://github.com/smkuhne))
## [2.0.0013](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0013) (2020-07-08)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0012...2.0.0013)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0012...2.0.0013)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0012...2.0.0013)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0012...2.0.0013)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0012...2.0.0013)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0012...2.0.0013)
**Merged pull requests:**
- 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))
- Use new warmup initializer [\#58](https://github.com/getbouncer/cardscan-demo-android/pull/58) ([xsl](https://github.com/xsl))
- Update UI [\#57](https://github.com/getbouncer/cardscan-demo-android/pull/57) ([awushensky](https://github.com/awushensky))
- Update for expiry extraction + new text detector [\#56](https://github.com/getbouncer/cardscan-demo-android/pull/56) ([xsl](https://github.com/xsl))
- Add more java interoperability [\#57](https://github.com/getbouncer/scan-framework-android/pull/57) ([awushensky](https://github.com/awushensky))
- Relocate loop state to result [\#56](https://github.com/getbouncer/scan-framework-android/pull/56) ([awushensky](https://github.com/awushensky))
- Add more memoize functions [\#55](https://github.com/getbouncer/scan-framework-android/pull/55) ([awushensky](https://github.com/awushensky))
- Separate error listeners [\#54](https://github.com/getbouncer/scan-framework-android/pull/54) ([awushensky](https://github.com/awushensky))
- Fix work leak [\#52](https://github.com/getbouncer/scan-framework-android/pull/52) ([awushensky](https://github.com/awushensky))
- Test duration [\#51](https://github.com/getbouncer/scan-framework-android/pull/51) ([awushensky](https://github.com/awushensky))
- Clean up network responses [\#49](https://github.com/getbouncer/scan-framework-android/pull/49) ([awushensky](https://github.com/awushensky))
- Add exceptions to retry [\#48](https://github.com/getbouncer/scan-framework-android/pull/48) ([awushensky](https://github.com/awushensky))
- 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))
- Add device identifier to network requests [\#45](https://github.com/getbouncer/scan-framework-android/pull/45) ([awushensky](https://github.com/awushensky))
- Standardize local file name [\#44](https://github.com/getbouncer/scan-framework-android/pull/44) ([awushensky](https://github.com/awushensky))
- Add some convenience functions [\#43](https://github.com/getbouncer/scan-framework-android/pull/43) ([xsl](https://github.com/xsl))
- Launch the camera on IO dispatcher [\#32](https://github.com/getbouncer/scan-camera-android/pull/32) ([awushensky](https://github.com/awushensky))
- Relocate analyzers [\#34](https://github.com/getbouncer/scan-payment-android/pull/34) ([awushensky](https://github.com/awushensky))
- Support multiple MM/YYs on cards [\#32](https://github.com/getbouncer/scan-payment-android/pull/32) ([xsl](https://github.com/xsl))
- 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))
- Expiry extraction + new text detector [\#30](https://github.com/getbouncer/scan-payment-android/pull/30) ([xsl](https://github.com/xsl))
- Update submodule [\#29](https://github.com/getbouncer/scan-ui-android/pull/29) ([awushensky](https://github.com/awushensky))
- Separate scan stats [\#27](https://github.com/getbouncer/scan-ui-android/pull/27) ([awushensky](https://github.com/awushensky))
- 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))
- Add initialization error strings [\#25](https://github.com/getbouncer/scan-ui-android/pull/25) ([xsl](https://github.com/xsl))
- Update UI [\#24](https://github.com/getbouncer/scan-ui-android/pull/24) ([awushensky](https://github.com/awushensky))
- Update scan-framework-android submodule [\#23](https://github.com/getbouncer/scan-ui-android/pull/23) ([xsl](https://github.com/xsl))
- Shut down analyzer context on quit [\#38](https://github.com/getbouncer/cardscan-ui-android/pull/38) ([awushensky](https://github.com/awushensky))
- Separate loop logic [\#37](https://github.com/getbouncer/cardscan-ui-android/pull/37) ([awushensky](https://github.com/awushensky))
- 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))
- Update ui [\#34](https://github.com/getbouncer/cardscan-ui-android/pull/34) ([awushensky](https://github.com/awushensky))
- Warmup name and expiry [\#33](https://github.com/getbouncer/cardscan-ui-android/pull/33) ([xsl](https://github.com/xsl))
- Expiry extraction + new text detector [\#32](https://github.com/getbouncer/cardscan-ui-android/pull/32) ([xsl](https://github.com/xsl))
## [2.0.0012](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0012) (2020-06-15)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0011...2.0.0012)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0011...2.0.0012)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0011...2.0.0012)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0011...2.0.0012)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0011...2.0.0012)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0011...2.0.0012)
**Merged pull requests:**
- Rename warm up [\#55](https://github.com/getbouncer/cardscan-demo-android/pull/55) ([awushensky](https://github.com/awushensky))
- Use flows instead of channels [\#42](https://github.com/getbouncer/scan-framework-android/pull/42) ([awushensky](https://github.com/awushensky))
- Use channels better [\#41](https://github.com/getbouncer/scan-framework-android/pull/41) ([awushensky](https://github.com/awushensky))
- Fix framerate average calculation [\#40](https://github.com/getbouncer/scan-framework-android/pull/40) ([awushensky](https://github.com/awushensky))
- Make result handlers listen to a lifecycle [\#39](https://github.com/getbouncer/scan-framework-android/pull/39) ([awushensky](https://github.com/awushensky))
- Move images out of framework [\#38](https://github.com/getbouncer/scan-framework-android/pull/38) ([awushensky](https://github.com/awushensky))
- Support java interop [\#37](https://github.com/getbouncer/scan-framework-android/pull/37) ([awushensky](https://github.com/awushensky))
- Fix camera crash on flash not supported [\#30](https://github.com/getbouncer/scan-camera-android/pull/30) ([awushensky](https://github.com/awushensky))
- Increase the channel buffer size to 2 [\#29](https://github.com/getbouncer/scan-camera-android/pull/29) ([awushensky](https://github.com/awushensky))
- Reintroduce camera2 [\#28](https://github.com/getbouncer/scan-camera-android/pull/28) ([awushensky](https://github.com/awushensky))
- Centralize the channel logic [\#27](https://github.com/getbouncer/scan-camera-android/pull/27) ([awushensky](https://github.com/awushensky))
- Remove framework submodule [\#26](https://github.com/getbouncer/scan-camera-android/pull/26) ([awushensky](https://github.com/awushensky))
- Add timing to ssdocr input [\#29](https://github.com/getbouncer/scan-payment-android/pull/29) ([awushensky](https://github.com/awushensky))
- Relocate test resources [\#28](https://github.com/getbouncer/scan-payment-android/pull/28) ([awushensky](https://github.com/awushensky))
- Relocate image manipulation utilities [\#27](https://github.com/getbouncer/scan-payment-android/pull/27) ([awushensky](https://github.com/awushensky))
- Use flows [\#22](https://github.com/getbouncer/scan-ui-android/pull/22) ([awushensky](https://github.com/awushensky))
- Relocate scan process [\#21](https://github.com/getbouncer/scan-ui-android/pull/21) ([awushensky](https://github.com/awushensky))
- Separate framework from camera [\#20](https://github.com/getbouncer/scan-ui-android/pull/20) ([awushensky](https://github.com/awushensky))
- Handle devices without cameras [\#19](https://github.com/getbouncer/scan-ui-android/pull/19) ([awushensky](https://github.com/awushensky))
- Reduce jitter in name display [\#31](https://github.com/getbouncer/cardscan-ui-android/pull/31) ([awushensky](https://github.com/awushensky))
- Fix analyzer pool memory leak [\#30](https://github.com/getbouncer/cardscan-ui-android/pull/30) ([awushensky](https://github.com/awushensky))
- Actually reset the result aggregator [\#29](https://github.com/getbouncer/cardscan-ui-android/pull/29) ([awushensky](https://github.com/awushensky))
- Relocate scan logic [\#28](https://github.com/getbouncer/cardscan-ui-android/pull/28) ([awushensky](https://github.com/awushensky))
- Relocate scan flow to implementation [\#27](https://github.com/getbouncer/cardscan-ui-android/pull/27) ([awushensky](https://github.com/awushensky))
- Separate camera and loops [\#26](https://github.com/getbouncer/cardscan-ui-android/pull/26) ([awushensky](https://github.com/awushensky))
## [2.0.0011](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0011) (2020-06-08)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0009...2.0.0011)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0010...2.0.0011)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0010...2.0.0011)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0010...2.0.0011)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0010...2.0.0011)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0009...2.0.0011)
**Merged pull requests:**
- Support separate name extraction parameters [\#54](https://github.com/getbouncer/cardscan-demo-android/pull/54) ([awushensky](https://github.com/awushensky))
- Add docs and buttons for name extraction [\#53](https://github.com/getbouncer/cardscan-demo-android/pull/53) ([xsl](https://github.com/xsl))
- Reduce name extraction settings [\#52](https://github.com/getbouncer/cardscan-demo-android/pull/52) ([awushensky](https://github.com/awushensky))
- 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))
- Update for name extraction [\#50](https://github.com/getbouncer/cardscan-demo-android/pull/50) ([xsl](https://github.com/xsl))
- 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))
- Update coroutine interoperability [\#36](https://github.com/getbouncer/scan-framework-android/pull/36) ([awushensky](https://github.com/awushensky))
- Standardize result counter [\#35](https://github.com/getbouncer/scan-framework-android/pull/35) ([awushensky](https://github.com/awushensky))
- 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))
- Add memoization functions [\#33](https://github.com/getbouncer/scan-framework-android/pull/33) ([awushensky](https://github.com/awushensky))
- Don't retry on FileNotFoundExceptions [\#32](https://github.com/getbouncer/scan-framework-android/pull/32) ([xsl](https://github.com/xsl))
- Update image utilities [\#31](https://github.com/getbouncer/scan-framework-android/pull/31) ([awushensky](https://github.com/awushensky))
- Use default dispatcher for camera [\#25](https://github.com/getbouncer/scan-camera-android/pull/25) ([awushensky](https://github.com/awushensky))
- 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))
- Update submodule [\#23](https://github.com/getbouncer/scan-camera-android/pull/23) ([xsl](https://github.com/xsl))
- Update image utils [\#22](https://github.com/getbouncer/scan-camera-android/pull/22) ([awushensky](https://github.com/awushensky))
- Remove threadsafe flag [\#26](https://github.com/getbouncer/scan-payment-android/pull/26) ([awushensky](https://github.com/awushensky))
- Minor tuning for name extraction [\#25](https://github.com/getbouncer/scan-payment-android/pull/25) ([xsl](https://github.com/xsl))
- Relocate object detect test [\#24](https://github.com/getbouncer/scan-payment-android/pull/24) ([awushensky](https://github.com/awushensky))
- Remove debug log [\#23](https://github.com/getbouncer/scan-payment-android/pull/23) ([awushensky](https://github.com/awushensky))
- 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))
- Update image utils [\#21](https://github.com/getbouncer/scan-payment-android/pull/21) ([awushensky](https://github.com/awushensky))
- 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))
- Remove unnecessary manifest entries [\#18](https://github.com/getbouncer/scan-ui-android/pull/18) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- Support java interoperability [\#25](https://github.com/getbouncer/cardscan-ui-android/pull/25) ([awushensky](https://github.com/awushensky))
- Enable disabling name extraction on start [\#24](https://github.com/getbouncer/cardscan-ui-android/pull/24) ([awushensky](https://github.com/awushensky))
- Update scan payments submodule [\#23](https://github.com/getbouncer/cardscan-ui-android/pull/23) ([xsl](https://github.com/xsl))
- Use default dispatchers [\#22](https://github.com/getbouncer/cardscan-ui-android/pull/22) ([awushensky](https://github.com/awushensky))
- Reduce settings for name extractor [\#21](https://github.com/getbouncer/cardscan-ui-android/pull/21) ([awushensky](https://github.com/awushensky))
- 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))
- Update image utils [\#19](https://github.com/getbouncer/cardscan-ui-android/pull/19) ([awushensky](https://github.com/awushensky))
- Name extraction v1 w/ old object detector [\#18](https://github.com/getbouncer/cardscan-ui-android/pull/18) ([xsl](https://github.com/xsl))
- 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))
## [2.0.0010](https://github.com/getbouncer/scan-framework-android/tree/2.0.0010) (2020-05-30)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0009...2.0.0010)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0009...2.0.0010)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0009...2.0.0010)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0009...2.0.0010)
**Merged pull requests:**
- Terminate a finite loop that has no data [\#30](https://github.com/getbouncer/scan-framework-android/pull/30) ([awushensky](https://github.com/awushensky))
- 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))
- Better support camera flash [\#21](https://github.com/getbouncer/scan-camera-android/pull/21) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- 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))
## [2.0.0009](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0009) (2020-05-29)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0008...2.0.0009)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0008...2.0.0009)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0008...2.0.0009)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0008...2.0.0009)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0008...2.0.0009)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0008...2.0.0009)
**Merged pull requests:**
- 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))
- Remove required card number [\#45](https://github.com/getbouncer/cardscan-demo-android/pull/45) ([awushensky](https://github.com/awushensky))
- 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))
- Start aggregation timer on valid result [\#27](https://github.com/getbouncer/scan-framework-android/pull/27) ([awushensky](https://github.com/awushensky))
- Stop ignoring scan timeout [\#26](https://github.com/getbouncer/scan-framework-android/pull/26) ([awushensky](https://github.com/awushensky))
- Simplify results [\#25](https://github.com/getbouncer/scan-framework-android/pull/25) ([awushensky](https://github.com/awushensky))
- Add logging to stats [\#24](https://github.com/getbouncer/scan-framework-android/pull/24) ([awushensky](https://github.com/awushensky))
- 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))
- Use random focus variance [\#18](https://github.com/getbouncer/scan-camera-android/pull/18) ([awushensky](https://github.com/awushensky))
- 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))
- Relocate aggregator [\#17](https://github.com/getbouncer/scan-payment-android/pull/17) ([awushensky](https://github.com/awushensky))
- 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))
- Keep the screen on while scanning [\#13](https://github.com/getbouncer/scan-ui-android/pull/13) ([awushensky](https://github.com/awushensky))
- Reset previously valid result [\#16](https://github.com/getbouncer/cardscan-ui-android/pull/16) ([awushensky](https://github.com/awushensky))
- 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))
- Remove isValidPan [\#14](https://github.com/getbouncer/cardscan-ui-android/pull/14) ([awushensky](https://github.com/awushensky))
- Start result aggregation on valid result [\#13](https://github.com/getbouncer/cardscan-ui-android/pull/13) ([awushensky](https://github.com/awushensky))
- Simplify results [\#12](https://github.com/getbouncer/cardscan-ui-android/pull/12) ([awushensky](https://github.com/awushensky))
## [2.0.0008](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0008) (2020-05-21)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0007...2.0.0008)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0007...2.0.0008)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0007...2.0.0008)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0007...2.0.0008)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0007...2.0.0008)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0007...2.0.0008)
**Merged pull requests:**
- 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))
- 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))
- 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))
- Make dependencies explicit [\#39](https://github.com/getbouncer/cardscan-demo-android/pull/39) ([awushensky](https://github.com/awushensky))
- Display card pan always [\#38](https://github.com/getbouncer/cardscan-demo-android/pull/38) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- Use invalid api key for test [\#14](https://github.com/getbouncer/scan-framework-android/pull/14) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- 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))
- Add more tests [\#8](https://github.com/getbouncer/scan-framework-android/pull/8) ([awushensky](https://github.com/awushensky))
- Add android test github action [\#7](https://github.com/getbouncer/scan-framework-android/pull/7) ([awushensky](https://github.com/awushensky))
- Ensure results are not duplicated [\#6](https://github.com/getbouncer/scan-framework-android/pull/6) ([awushensky](https://github.com/awushensky))
- Optimize some image utilities [\#5](https://github.com/getbouncer/scan-framework-android/pull/5) ([awushensky](https://github.com/awushensky))
- Refocus camera [\#15](https://github.com/getbouncer/scan-camera-android/pull/15) ([awushensky](https://github.com/awushensky))
- Simplify camera start [\#14](https://github.com/getbouncer/scan-camera-android/pull/14) ([awushensky](https://github.com/awushensky))
- Ignore camera config change failures [\#13](https://github.com/getbouncer/scan-camera-android/pull/13) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- 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))
- 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))
- Add tests to camera [\#5](https://github.com/getbouncer/scan-camera-android/pull/5) ([awushensky](https://github.com/awushensky))
- Remove camerax and camera2 [\#4](https://github.com/getbouncer/scan-camera-android/pull/4) ([awushensky](https://github.com/awushensky))
- Add camera1 and camerax [\#3](https://github.com/getbouncer/scan-camera-android/pull/3) ([awushensky](https://github.com/awushensky))
- Use better coroutine testing [\#13](https://github.com/getbouncer/scan-payment-android/pull/13) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- 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))
- 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))
- Add tests [\#5](https://github.com/getbouncer/scan-payment-android/pull/5) ([awushensky](https://github.com/awushensky))
- 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))
- Add tests [\#10](https://github.com/getbouncer/scan-ui-android/pull/10) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- Update default user interface [\#6](https://github.com/getbouncer/scan-ui-android/pull/6) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- 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))
- Add integration tests to CI [\#6](https://github.com/getbouncer/cardscan-ui-android/pull/6) ([awushensky](https://github.com/awushensky))
- Update user interface [\#5](https://github.com/getbouncer/cardscan-ui-android/pull/5) ([awushensky](https://github.com/awushensky))
## [2.0.0007](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0007) (2020-05-12)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0006...2.0.0007)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0006...2.0.0007)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0006...2.0.0007)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0006...2.0.0007)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0006...2.0.0007)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0005...2.0.0007)
**Merged pull requests:**
- Update documentation [\#37](https://github.com/getbouncer/cardscan-demo-android/pull/37) ([awushensky](https://github.com/awushensky))
- Fix crash on network failure [\#4](https://github.com/getbouncer/scan-framework-android/pull/4) ([awushensky](https://github.com/awushensky))
- Fix crash on camera open failure [\#2](https://github.com/getbouncer/scan-camera-android/pull/2) ([awushensky](https://github.com/awushensky))
- Update submodules [\#4](https://github.com/getbouncer/scan-payment-android/pull/4) ([awushensky](https://github.com/awushensky))
- Update version [\#5](https://github.com/getbouncer/scan-ui-android/pull/5) ([awushensky](https://github.com/awushensky))
- Update submodules [\#4](https://github.com/getbouncer/cardscan-ui-android/pull/4) ([awushensky](https://github.com/awushensky))
- Set api key on warmup [\#3](https://github.com/getbouncer/cardscan-ui-android/pull/3) ([awushensky](https://github.com/awushensky))
## [2.0.0006](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0006) (2020-05-09)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0005...2.0.0006)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0005...2.0.0006)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0005...2.0.0006)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0005...2.0.0006)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.005...2.0.0006)
**Merged pull requests:**
- Update version [\#36](https://github.com/getbouncer/cardscan-demo-android/pull/36) ([awushensky](https://github.com/awushensky))
- Fix signedUrl failure crash [\#3](https://github.com/getbouncer/scan-framework-android/pull/3) ([awushensky](https://github.com/awushensky))
- update version [\#3](https://github.com/getbouncer/scan-payment-android/pull/3) ([awushensky](https://github.com/awushensky))
- Allow invalid api key error [\#4](https://github.com/getbouncer/scan-ui-android/pull/4) ([awushensky](https://github.com/awushensky))
## [2.0.0005](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0005) (2020-05-08)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0004...2.0.0005)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/e0e5089a945c8b6b6ed47f3838d3d61997c1afe4...2.0.0005)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/70e9702f2bb0a99ef89e1477064eb541b661170f...2.0.0005)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/d19502708ef72c01d522e2d18e7a8bfbb81ec0b2...2.0.0005)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/0904ef185c19491c25b73c61eb8a22bed1eecb75...2.0.005)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/ebb299b0e1b799ec7c12aff1f535d0278d9107c1...2.0.0005)
**Merged pull requests:**
- Update submodules [\#35](https://github.com/getbouncer/cardscan-demo-android/pull/35) ([awushensky](https://github.com/awushensky))
- Replace libraries [\#34](https://github.com/getbouncer/cardscan-demo-android/pull/34) ([awushensky](https://github.com/awushensky))
- Remove build from git [\#2](https://github.com/getbouncer/scan-framework-android/pull/2) ([awushensky](https://github.com/awushensky))
- Update documentation [\#1](https://github.com/getbouncer/scan-framework-android/pull/1) ([awushensky](https://github.com/awushensky))
- Update documentation [\#1](https://github.com/getbouncer/scan-camera-android/pull/1) ([awushensky](https://github.com/awushensky))
- Add results [\#2](https://github.com/getbouncer/scan-payment-android/pull/2) ([awushensky](https://github.com/awushensky))
- Update documentation [\#1](https://github.com/getbouncer/scan-payment-android/pull/1) ([awushensky](https://github.com/awushensky))
- Rename module in docs [\#3](https://github.com/getbouncer/scan-ui-android/pull/3) ([awushensky](https://github.com/awushensky))
- Rename module [\#2](https://github.com/getbouncer/scan-ui-android/pull/2) ([awushensky](https://github.com/awushensky))
- Update docs [\#1](https://github.com/getbouncer/scan-ui-android/pull/1) ([awushensky](https://github.com/awushensky))
- Update submodules [\#2](https://github.com/getbouncer/cardscan-ui-android/pull/2) ([awushensky](https://github.com/awushensky))
- Update documentation [\#1](https://github.com/getbouncer/cardscan-ui-android/pull/1) ([awushensky](https://github.com/awushensky))
## [2.0.0004](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0004) (2020-05-06)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0003...2.0.0004)
**Merged pull requests:**
- Update version [\#33](https://github.com/getbouncer/cardscan-demo-android/pull/33) ([awushensky](https://github.com/awushensky))
## [2.0.0003](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0003) (2020-04-29)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/a0e3c8e303b7f72e2b08701e01781d9558b07e2c...2.0.0003)
**Implemented enhancements:**
- Update github checks [\#5](https://github.com/getbouncer/cardscan-demo-android/pull/5) ([awushensky](https://github.com/awushensky))
**Merged pull requests:**
- Update thread handling [\#32](https://github.com/getbouncer/cardscan-demo-android/pull/32) ([awushensky](https://github.com/awushensky))
- Update documentation [\#31](https://github.com/getbouncer/cardscan-demo-android/pull/31) ([awushensky](https://github.com/awushensky))
- Extract common ui [\#30](https://github.com/getbouncer/cardscan-demo-android/pull/30) ([awushensky](https://github.com/awushensky))
- Update submodules [\#29](https://github.com/getbouncer/cardscan-demo-android/pull/29) ([awushensky](https://github.com/awushensky))
- Update submodules [\#28](https://github.com/getbouncer/cardscan-demo-android/pull/28) ([awushensky](https://github.com/awushensky))
- Update submodules [\#27](https://github.com/getbouncer/cardscan-demo-android/pull/27) ([awushensky](https://github.com/awushensky))
- Update submodules [\#26](https://github.com/getbouncer/cardscan-demo-android/pull/26) ([awushensky](https://github.com/awushensky))
- Update submodules [\#25](https://github.com/getbouncer/cardscan-demo-android/pull/25) ([awushensky](https://github.com/awushensky))
- Update submodules [\#24](https://github.com/getbouncer/cardscan-demo-android/pull/24) ([awushensky](https://github.com/awushensky))
- Update submodules [\#23](https://github.com/getbouncer/cardscan-demo-android/pull/23) ([awushensky](https://github.com/awushensky))
- Prevent screenshots [\#22](https://github.com/getbouncer/cardscan-demo-android/pull/22) ([awushensky](https://github.com/awushensky))
- Add api key check [\#21](https://github.com/getbouncer/cardscan-demo-android/pull/21) ([awushensky](https://github.com/awushensky))
- Update license [\#20](https://github.com/getbouncer/cardscan-demo-android/pull/20) ([awushensky](https://github.com/awushensky))
- Update documentation [\#19](https://github.com/getbouncer/cardscan-demo-android/pull/19) ([awushensky](https://github.com/awushensky))
- Update submodules [\#18](https://github.com/getbouncer/cardscan-demo-android/pull/18) ([awushensky](https://github.com/awushensky))
- Add documentation [\#17](https://github.com/getbouncer/cardscan-demo-android/pull/17) ([awushensky](https://github.com/awushensky))
- Make logo optional [\#16](https://github.com/getbouncer/cardscan-demo-android/pull/16) ([awushensky](https://github.com/awushensky))
- Update submodules [\#15](https://github.com/getbouncer/cardscan-demo-android/pull/15) ([awushensky](https://github.com/awushensky))
- Update submodules [\#14](https://github.com/getbouncer/cardscan-demo-android/pull/14) ([awushensky](https://github.com/awushensky))
- Rename app to demo [\#13](https://github.com/getbouncer/cardscan-demo-android/pull/13) ([awushensky](https://github.com/awushensky))
- Support state in loops [\#12](https://github.com/getbouncer/cardscan-demo-android/pull/12) ([awushensky](https://github.com/awushensky))
- Remove camera 1 api [\#11](https://github.com/getbouncer/cardscan-demo-android/pull/11) ([awushensky](https://github.com/awushensky))
- Scope tests to the app itself [\#10](https://github.com/getbouncer/cardscan-demo-android/pull/10) ([awushensky](https://github.com/awushensky))
- Update submodules [\#9](https://github.com/getbouncer/cardscan-demo-android/pull/9) ([awushensky](https://github.com/awushensky))
- Update submodules [\#8](https://github.com/getbouncer/cardscan-demo-android/pull/8) ([awushensky](https://github.com/awushensky))
- Use submodules [\#7](https://github.com/getbouncer/cardscan-demo-android/pull/7) ([awushensky](https://github.com/awushensky))
- Show the card pan when scanninng [\#6](https://github.com/getbouncer/cardscan-demo-android/pull/6) ([awushensky](https://github.com/awushensky))
- Update dependencies [\#4](https://github.com/getbouncer/cardscan-demo-android/pull/4) ([awushensky](https://github.com/awushensky))
- Require API key [\#3](https://github.com/getbouncer/cardscan-demo-android/pull/3) ([awushensky](https://github.com/awushensky))
- Add code owners [\#2](https://github.com/getbouncer/cardscan-demo-android/pull/2) ([awushensky](https://github.com/awushensky))
- Support camera1 APIs [\#1](https://github.com/getbouncer/cardscan-demo-android/pull/1) ([awushensky](https://github.com/awushensky))
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
================================================
FILE: CODEOWNERS
================================================
# This is a comment.
# Each line is a file pattern followed by one or more owners.
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
# @global-owner1 and @global-owner2 will be requested for
# review when someone opens a pull request.
* @awushensky @xsl @kingst @dxaen @awushensky-stripe
# Order is important; the last matching pattern takes the most
# precedence. When someone opens a pull request that only
# modifies JS files, only @js-owner and not the global
# owner(s) will be requested for a review.
# *.js @js-owner
# You can also use email addresses if you prefer. They'll be
# used to look up users just like we do for commit author
# emails.
# *.go docs@example.com
# In this example, @doctocat owns any files in the build/logs
# directory at the root of the repository and any of its
# subdirectories.
# /build/logs/ @doctocat
# The `docs/*` pattern will match files like
# `docs/getting-started.md` but not further nested files like
# `docs/build-app/troubleshooting.md`.
# docs/* docs@example.com
# In this example, @octocat owns any file in an apps directory
# anywhere in your repository.
# apps/ @octocat
# In this example, @doctocat owns any file in the `/docs`
# directory in the root of your repository.
# /docs/ @doctocat
================================================
FILE: Contributor License Agreement
================================================
Bouncer
Individual and Entity Contributor License Agreement
Thank you for your interest in contributing to software projects managed by Bouncer Technologies,
Inc. (“We”, “Us” or “Our”). This Contributor License Agreement (“Agreement”) documents the rights
granted by contributors to Us. This Agreement is for your protection as a contributor as well as for
our protection; it does not change your rights to use your own Contributions for any other purpose.
To make this document effective, please read the Agreement carefully and then (i) sign it and send
it to Us by email (PDF) at bouncer-support@stripe.com or (ii) submit it to us electronically, in either
case by following the instructions at [insert hyperlink]. By signing this Agreement (including by
clicking “I agree” and submitting it to us electronically), You are creating a legally binding
contract. If You are less than eighteen years old, please have Your parents or guardian sign the
Agreement. This Agreement covers all future Contributions from You, and may cover more than one
software project managed by Us.
1. Definitions
--------------
“Affiliates” means other Legal Entities that control, are controlled by, or under common control
with that Legal Entity. For the purposes of this definition, “control” means (i) the power, direct
or indirect, to cause the direction or management of such Legal Entity, whether by contract or
otherwise, (ii) ownership of fifty percent (50%) or more of the outstanding shares or securities
which vote to elect the management or other persons who direct such Legal Entity or (iii) beneficial
ownership of such entity.
“Contribution” means any work of authorship that is Submitted by You to Us in which You own or
assert ownership of the Copyright. By Submitting any Contribution, you represent that You own the
Copyright in the entire work of authorship, or that you otherwise are legally entitled to Submit the
Contribution and to grant the licenses in this Agreement.
“Copyright” means all rights protecting works of authorship owned or controlled by You or your
Affiliates (as may be applicable), including copyright, moral and related (or neighboring) rights,
as appropriate, for the full term of their existence, including any extensions by You.
“Effective Date” means the date You execute this Agreement or the date You first Submit a
Contribution to Us, whichever is earlier.
“Legal Entity” means an entity which is not a natural person.
“Material” means the work of authorship which is made available by Us to third parties. When this
Agreement covers more than one software project, the Material means the work of authorship to which
the Contribution was Submitted. After You Submit the Contribution, it may be included in the
Material.
“Media” means any portion of a Contribution which is not software.
“Submit” means any form of electronic, verbal, or written communication sent to Us or our
representatives at a destination (including websites) that we own or control or that is otherwise
registered to us, including but not limited to electronic mailing lists, source code control
systems, instant messages or similar communications, and issue tracking systems that are managed by,
or on behalf of, Us for the purpose of discussing and improving the Material, but excluding any
communication that is conspicuously marked or otherwise designated in writing by You as “Not a
Contribution.”
“Submission Date” means the date on which You Submit a Contribution to Us.
“You” (entity). If You are an individual acting on your own behalf, then “You” means the individual
who Submits a Contribution to Us.
“You” (individual). If You are Submitting any Contribution on behalf of any entity, then “You” means
the Legal Entity on behalf of whom you Submit a Contribution to Us.
2. Grant of Rights
------------------
2.1 Copyright License
(a) Except for the license granted to Us in this Agreement, You reserve all right, title, and
interest in and to Your Contributions. That means that you can keep doing whatever you want with
your Contribution, and you can license it to anyone you want under any terms you want.
(b) To the maximum extent permitted by the relevant law, You grant to Us a perpetual, worldwide,
non-exclusive, transferable, no charge and royalty-free, irrevocable license under the Copyright
covering the Contribution, with the right to sublicense such rights through multiple tiers of
sublicensees, to reproduce, modify, display, perform, sublicense and distribute the Contribution as
part of the Material; provided that this license is subject to Section 2.3.
2.2 Patent License
For patent claims including, without limitation, method, process, and apparatus claims which You (or
your Affiliates, as may be applicable) own, control or have the right to grant, now or in the
future, You grant to Us a perpetual, worldwide, non-exclusive, transferable, no charge and royalty-
free, irrevocable patent license, with the right to sublicense these rights to multiple tiers of
sublicensees, to make, have made, use, sell, offer for sale, import and otherwise transfer the
Contribution (and the Contribution in combination with the Material, and portions of such
combination). This license is granted only to the extent that the exercise of the licensed rights
infringes such patent claims; and is subject to Section 2.3. If any person institutes patent
litigation against Contributor or any other entity (including a cross-claim or counterclaim in a
lawsuit) alleging that the Contributions, or the Project to which the Contributions were submitted,
constitutes direct or contributory patent infringement, then any patent licenses granted under this
Agreement for that Contribution to the person or entity instituting the litigation, or the Project
to which the Contributions were submitted, shall terminate as of the date such litigation is filed.
2.3 Outbound License
Based on the grant of rights in Sections 2.1 (meaning, no matter what, you can keep licensing your
Contribution to others however you want) and 2.2, if We include Your Contribution in any Material,
and if We determine that it is appropriate for the purpose of commercializing any Material or any
project under Our control, we may license the Contribution under any license, including copyleft,
permissive, commercial, or proprietary licenses.
2.4 Moral Rights.
We agree to comply with applicable laws regarding your Contribution, including copyright laws and
law related to moral rights. If moral rights apply to the Contribution, to the maximum extent
permitted by law, You waive and agree not to assert such moral rights against Us or our successors
in interest, or any of our licensees, either direct or indirect.
2.5 Our Rights.
You acknowledge that We are not obligated to use Your Contribution as part of any Material, and that
we and may decide to include any Contribution We consider appropriate.
2.6 Reservation of Rights.
Any rights in Your Contribution not expressly licensed under this Agreement are expressly reserved
by You.
3. Agreement.
-------------
You confirm that:
(a) You have the legal authority to enter into this Agreement. If your employer(s) has rights to
intellectual property that you create that includes your Contributions, you represent that you have
received permission to make Contributions on behalf of that employer, and that your employer has
waived such rights for your Contributions to Us.
(b) You (or your Affiliates, as may be applicable) own or otherwise have the legal right to license
the Copyright and patent claims covering the Contribution which are required to grant the rights
under Section 2.
(c) The grant of rights under Section 2 does not violate any grant of rights which You (or your
Affiliates, as may be applicable) have made to third parties, including Your employer.
(d) You represent that each of Your Contributions is Your original work. You represent that Your
Contribution submissions include complete details of any third-party license or other restriction
(including, but not limited to, related patents and trademarks) of which you are personally aware
and which are associated with any part of Your Contributions.
(e) You agree to notify Us of any facts or circumstances of which you become aware that would make
these representations inaccurate in any respect.
4. Disclaimer
-------------
EXCEPT FOR THE EXPRESS WARRANTIES IN SECTION 3, THE CONTRIBUTION IS PROVIDED "AS IS". YOU EXPRESSLY
DISCLAIM ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. TO THE EXTENT THAT ANY SUCH
WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION TO THE MINIMUM PERIOD
PERMITTED BY LAW.
5. Consequential Damage Waiver
------------------------------
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU OR WE BE LIABLE FOR ANY LOSS
OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL
AND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY
(CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED.
6. Miscellaneous
----------------
6.1 This Agreement will be governed by and construed in accordance with the laws of the State of
California, without regard to conflicts of law provisions. The sole venue for all disputes relating
to this Agreement shall be in Alameda County, California. The rights and obligations of the parties
under this Agreement shall not be governed by the 1980 U.N. Convention on Contracts for the
International Sale of Goods.
6.2 This Agreement may be amended only by a written document signed by the party against whom
enforcement is sought.
6.3 The failure of either party to require performance by the other party of any provision of this
Agreement in one situation shall not affect the right of a party to require such performance at any
time in the future. A waiver of performance under a provision in one situation shall not be
considered a waiver of the performance of the provision in the future or a waiver of the provision
in its entirety.
6.4 If any provision of this Agreement is found void and unenforceable, such provision will be
replaced to the extent possible with a provision that comes closest to the meaning of the original
provision and which is enforceable. The terms and conditions set forth in this Agreement shall apply
notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the
maximum extent possible under law.
This Agreement contains the entire understanding of the parties regarding the subject matter of this
Agreement and supersedes all prior and contemporaneous negotiations and agreements, whether written
or oral, between the parties with respect to the subject matter of this Agreement.
By signing below, Contributor accepts and agrees to the preceding terms and conditions for
Contributor’s present and future Contributions submitted to Us.
________________________________________
Signature
________________________________________
Contributor Name
________________________________________
Legal Entity Name (if applicable)
________________________________________
Contributor Address
________________________________________
Title
________________________________________
Contributor Address
________________________________________
Email
________________________________________
Telephone
________________________________________
Date
================================================
FILE: HISTORY.md
================================================
## [2.0.0014](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0014) (2020-07-14)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0013...2.0.0014)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0013...2.0.0014)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0013...2.0.0014)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0013...2.0.0014)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0013...2.0.0014)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0013...2.0.0014)
**Merged pull requests:**
- Image fragmentation [\#53](https://github.com/getbouncer/scan-framework-android/pull/53) ([smkuhne](https://github.com/smkuhne))
- Standardize models [\#35](https://github.com/getbouncer/scan-payment-android/pull/35) ([awushensky](https://github.com/awushensky))
- Image fragmentation [\#33](https://github.com/getbouncer/scan-payment-android/pull/33) ([smkuhne](https://github.com/smkuhne))
- Allow extending uploadstats [\#30](https://github.com/getbouncer/scan-ui-android/pull/30) ([awushensky](https://github.com/awushensky))
- Image fragmentation [\#28](https://github.com/getbouncer/scan-ui-android/pull/28) ([smkuhne](https://github.com/smkuhne))
## [2.0.0013](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0013) (2020-07-08)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0012...2.0.0013)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0012...2.0.0013)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0012...2.0.0013)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0012...2.0.0013)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0012...2.0.0013)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0012...2.0.0013)
**Merged pull requests:**
- 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))
- Use new warmup initializer [\#58](https://github.com/getbouncer/cardscan-demo-android/pull/58) ([xsl](https://github.com/xsl))
- Update UI [\#57](https://github.com/getbouncer/cardscan-demo-android/pull/57) ([awushensky](https://github.com/awushensky))
- Update for expiry extraction + new text detector [\#56](https://github.com/getbouncer/cardscan-demo-android/pull/56) ([xsl](https://github.com/xsl))
- Add more java interoperability [\#57](https://github.com/getbouncer/scan-framework-android/pull/57) ([awushensky](https://github.com/awushensky))
- Relocate loop state to result [\#56](https://github.com/getbouncer/scan-framework-android/pull/56) ([awushensky](https://github.com/awushensky))
- Add more memoize functions [\#55](https://github.com/getbouncer/scan-framework-android/pull/55) ([awushensky](https://github.com/awushensky))
- Separate error listeners [\#54](https://github.com/getbouncer/scan-framework-android/pull/54) ([awushensky](https://github.com/awushensky))
- Fix work leak [\#52](https://github.com/getbouncer/scan-framework-android/pull/52) ([awushensky](https://github.com/awushensky))
- Test duration [\#51](https://github.com/getbouncer/scan-framework-android/pull/51) ([awushensky](https://github.com/awushensky))
- Clean up network responses [\#49](https://github.com/getbouncer/scan-framework-android/pull/49) ([awushensky](https://github.com/awushensky))
- Add exceptions to retry [\#48](https://github.com/getbouncer/scan-framework-android/pull/48) ([awushensky](https://github.com/awushensky))
- 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))
- Add device identifier to network requests [\#45](https://github.com/getbouncer/scan-framework-android/pull/45) ([awushensky](https://github.com/awushensky))
- Standardize local file name [\#44](https://github.com/getbouncer/scan-framework-android/pull/44) ([awushensky](https://github.com/awushensky))
- Add some convenience functions [\#43](https://github.com/getbouncer/scan-framework-android/pull/43) ([xsl](https://github.com/xsl))
- Launch the camera on IO dispatcher [\#32](https://github.com/getbouncer/scan-camera-android/pull/32) ([awushensky](https://github.com/awushensky))
- Relocate analyzers [\#34](https://github.com/getbouncer/scan-payment-android/pull/34) ([awushensky](https://github.com/awushensky))
- Support multiple MM/YYs on cards [\#32](https://github.com/getbouncer/scan-payment-android/pull/32) ([xsl](https://github.com/xsl))
- 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))
- Expiry extraction + new text detector [\#30](https://github.com/getbouncer/scan-payment-android/pull/30) ([xsl](https://github.com/xsl))
- Update submodule [\#29](https://github.com/getbouncer/scan-ui-android/pull/29) ([awushensky](https://github.com/awushensky))
- Separate scan stats [\#27](https://github.com/getbouncer/scan-ui-android/pull/27) ([awushensky](https://github.com/awushensky))
- 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))
- Add initialization error strings [\#25](https://github.com/getbouncer/scan-ui-android/pull/25) ([xsl](https://github.com/xsl))
- Update UI [\#24](https://github.com/getbouncer/scan-ui-android/pull/24) ([awushensky](https://github.com/awushensky))
- Update scan-framework-android submodule [\#23](https://github.com/getbouncer/scan-ui-android/pull/23) ([xsl](https://github.com/xsl))
- Shut down analyzer context on quit [\#38](https://github.com/getbouncer/cardscan-ui-android/pull/38) ([awushensky](https://github.com/awushensky))
- Separate loop logic [\#37](https://github.com/getbouncer/cardscan-ui-android/pull/37) ([awushensky](https://github.com/awushensky))
- 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))
- Update ui [\#34](https://github.com/getbouncer/cardscan-ui-android/pull/34) ([awushensky](https://github.com/awushensky))
- Warmup name and expiry [\#33](https://github.com/getbouncer/cardscan-ui-android/pull/33) ([xsl](https://github.com/xsl))
- Expiry extraction + new text detector [\#32](https://github.com/getbouncer/cardscan-ui-android/pull/32) ([xsl](https://github.com/xsl))
## [2.0.0012](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0012) (2020-06-15)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0011...2.0.0012)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0011...2.0.0012)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0011...2.0.0012)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0011...2.0.0012)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0011...2.0.0012)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0011...2.0.0012)
**Merged pull requests:**
- Rename warm up [\#55](https://github.com/getbouncer/cardscan-demo-android/pull/55) ([awushensky](https://github.com/awushensky))
- Use flows instead of channels [\#42](https://github.com/getbouncer/scan-framework-android/pull/42) ([awushensky](https://github.com/awushensky))
- Use channels better [\#41](https://github.com/getbouncer/scan-framework-android/pull/41) ([awushensky](https://github.com/awushensky))
- Fix framerate average calculation [\#40](https://github.com/getbouncer/scan-framework-android/pull/40) ([awushensky](https://github.com/awushensky))
- Make result handlers listen to a lifecycle [\#39](https://github.com/getbouncer/scan-framework-android/pull/39) ([awushensky](https://github.com/awushensky))
- Move images out of framework [\#38](https://github.com/getbouncer/scan-framework-android/pull/38) ([awushensky](https://github.com/awushensky))
- Support java interop [\#37](https://github.com/getbouncer/scan-framework-android/pull/37) ([awushensky](https://github.com/awushensky))
- Fix camera crash on flash not supported [\#30](https://github.com/getbouncer/scan-camera-android/pull/30) ([awushensky](https://github.com/awushensky))
- Increase the channel buffer size to 2 [\#29](https://github.com/getbouncer/scan-camera-android/pull/29) ([awushensky](https://github.com/awushensky))
- Reintroduce camera2 [\#28](https://github.com/getbouncer/scan-camera-android/pull/28) ([awushensky](https://github.com/awushensky))
- Centralize the channel logic [\#27](https://github.com/getbouncer/scan-camera-android/pull/27) ([awushensky](https://github.com/awushensky))
- Remove framework submodule [\#26](https://github.com/getbouncer/scan-camera-android/pull/26) ([awushensky](https://github.com/awushensky))
- Add timing to ssdocr input [\#29](https://github.com/getbouncer/scan-payment-android/pull/29) ([awushensky](https://github.com/awushensky))
- Relocate test resources [\#28](https://github.com/getbouncer/scan-payment-android/pull/28) ([awushensky](https://github.com/awushensky))
- Relocate image manipulation utilities [\#27](https://github.com/getbouncer/scan-payment-android/pull/27) ([awushensky](https://github.com/awushensky))
- Use flows [\#22](https://github.com/getbouncer/scan-ui-android/pull/22) ([awushensky](https://github.com/awushensky))
- Relocate scan process [\#21](https://github.com/getbouncer/scan-ui-android/pull/21) ([awushensky](https://github.com/awushensky))
- Separate framework from camera [\#20](https://github.com/getbouncer/scan-ui-android/pull/20) ([awushensky](https://github.com/awushensky))
- Handle devices without cameras [\#19](https://github.com/getbouncer/scan-ui-android/pull/19) ([awushensky](https://github.com/awushensky))
- Reduce jitter in name display [\#31](https://github.com/getbouncer/cardscan-ui-android/pull/31) ([awushensky](https://github.com/awushensky))
- Fix analyzer pool memory leak [\#30](https://github.com/getbouncer/cardscan-ui-android/pull/30) ([awushensky](https://github.com/awushensky))
- Actually reset the result aggregator [\#29](https://github.com/getbouncer/cardscan-ui-android/pull/29) ([awushensky](https://github.com/awushensky))
- Relocate scan logic [\#28](https://github.com/getbouncer/cardscan-ui-android/pull/28) ([awushensky](https://github.com/awushensky))
- Relocate scan flow to implementation [\#27](https://github.com/getbouncer/cardscan-ui-android/pull/27) ([awushensky](https://github.com/awushensky))
- Separate camera and loops [\#26](https://github.com/getbouncer/cardscan-ui-android/pull/26) ([awushensky](https://github.com/awushensky))
## [2.0.0011](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0011) (2020-06-08)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0009...2.0.0011)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0010...2.0.0011)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0010...2.0.0011)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0010...2.0.0011)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0010...2.0.0011)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0009...2.0.0011)
**Merged pull requests:**
- Support separate name extraction parameters [\#54](https://github.com/getbouncer/cardscan-demo-android/pull/54) ([awushensky](https://github.com/awushensky))
- Add docs and buttons for name extraction [\#53](https://github.com/getbouncer/cardscan-demo-android/pull/53) ([xsl](https://github.com/xsl))
- Reduce name extraction settings [\#52](https://github.com/getbouncer/cardscan-demo-android/pull/52) ([awushensky](https://github.com/awushensky))
- 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))
- Update for name extraction [\#50](https://github.com/getbouncer/cardscan-demo-android/pull/50) ([xsl](https://github.com/xsl))
- 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))
- Update coroutine interoperability [\#36](https://github.com/getbouncer/scan-framework-android/pull/36) ([awushensky](https://github.com/awushensky))
- Standardize result counter [\#35](https://github.com/getbouncer/scan-framework-android/pull/35) ([awushensky](https://github.com/awushensky))
- 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))
- Add memoization functions [\#33](https://github.com/getbouncer/scan-framework-android/pull/33) ([awushensky](https://github.com/awushensky))
- Don't retry on FileNotFoundExceptions [\#32](https://github.com/getbouncer/scan-framework-android/pull/32) ([xsl](https://github.com/xsl))
- Update image utilities [\#31](https://github.com/getbouncer/scan-framework-android/pull/31) ([awushensky](https://github.com/awushensky))
- Use default dispatcher for camera [\#25](https://github.com/getbouncer/scan-camera-android/pull/25) ([awushensky](https://github.com/awushensky))
- 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))
- Update submodule [\#23](https://github.com/getbouncer/scan-camera-android/pull/23) ([xsl](https://github.com/xsl))
- Update image utils [\#22](https://github.com/getbouncer/scan-camera-android/pull/22) ([awushensky](https://github.com/awushensky))
- Remove threadsafe flag [\#26](https://github.com/getbouncer/scan-payment-android/pull/26) ([awushensky](https://github.com/awushensky))
- Minor tuning for name extraction [\#25](https://github.com/getbouncer/scan-payment-android/pull/25) ([xsl](https://github.com/xsl))
- Relocate object detect test [\#24](https://github.com/getbouncer/scan-payment-android/pull/24) ([awushensky](https://github.com/awushensky))
- Remove debug log [\#23](https://github.com/getbouncer/scan-payment-android/pull/23) ([awushensky](https://github.com/awushensky))
- 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))
- Update image utils [\#21](https://github.com/getbouncer/scan-payment-android/pull/21) ([awushensky](https://github.com/awushensky))
- 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))
- Remove unnecessary manifest entries [\#18](https://github.com/getbouncer/scan-ui-android/pull/18) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- Support java interoperability [\#25](https://github.com/getbouncer/cardscan-ui-android/pull/25) ([awushensky](https://github.com/awushensky))
- Enable disabling name extraction on start [\#24](https://github.com/getbouncer/cardscan-ui-android/pull/24) ([awushensky](https://github.com/awushensky))
- Update scan payments submodule [\#23](https://github.com/getbouncer/cardscan-ui-android/pull/23) ([xsl](https://github.com/xsl))
- Use default dispatchers [\#22](https://github.com/getbouncer/cardscan-ui-android/pull/22) ([awushensky](https://github.com/awushensky))
- Reduce settings for name extractor [\#21](https://github.com/getbouncer/cardscan-ui-android/pull/21) ([awushensky](https://github.com/awushensky))
- 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))
- Update image utils [\#19](https://github.com/getbouncer/cardscan-ui-android/pull/19) ([awushensky](https://github.com/awushensky))
- Name extraction v1 w/ old object detector [\#18](https://github.com/getbouncer/cardscan-ui-android/pull/18) ([xsl](https://github.com/xsl))
- 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))
## [2.0.0010](https://github.com/getbouncer/scan-framework-android/tree/2.0.0010) (2020-05-30)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0009...2.0.0010)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0009...2.0.0010)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0009...2.0.0010)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0009...2.0.0010)
**Merged pull requests:**
- Terminate a finite loop that has no data [\#30](https://github.com/getbouncer/scan-framework-android/pull/30) ([awushensky](https://github.com/awushensky))
- 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))
- Better support camera flash [\#21](https://github.com/getbouncer/scan-camera-android/pull/21) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- 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))
## [2.0.0009](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0009) (2020-05-29)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0008...2.0.0009)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0008...2.0.0009)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0008...2.0.0009)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0008...2.0.0009)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0008...2.0.0009)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0008...2.0.0009)
**Merged pull requests:**
- 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))
- Remove required card number [\#45](https://github.com/getbouncer/cardscan-demo-android/pull/45) ([awushensky](https://github.com/awushensky))
- 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))
- Start aggregation timer on valid result [\#27](https://github.com/getbouncer/scan-framework-android/pull/27) ([awushensky](https://github.com/awushensky))
- Stop ignoring scan timeout [\#26](https://github.com/getbouncer/scan-framework-android/pull/26) ([awushensky](https://github.com/awushensky))
- Simplify results [\#25](https://github.com/getbouncer/scan-framework-android/pull/25) ([awushensky](https://github.com/awushensky))
- Add logging to stats [\#24](https://github.com/getbouncer/scan-framework-android/pull/24) ([awushensky](https://github.com/awushensky))
- 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))
- Use random focus variance [\#18](https://github.com/getbouncer/scan-camera-android/pull/18) ([awushensky](https://github.com/awushensky))
- 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))
- Relocate aggregator [\#17](https://github.com/getbouncer/scan-payment-android/pull/17) ([awushensky](https://github.com/awushensky))
- 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))
- Keep the screen on while scanning [\#13](https://github.com/getbouncer/scan-ui-android/pull/13) ([awushensky](https://github.com/awushensky))
- Reset previously valid result [\#16](https://github.com/getbouncer/cardscan-ui-android/pull/16) ([awushensky](https://github.com/awushensky))
- 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))
- Remove isValidPan [\#14](https://github.com/getbouncer/cardscan-ui-android/pull/14) ([awushensky](https://github.com/awushensky))
- Start result aggregation on valid result [\#13](https://github.com/getbouncer/cardscan-ui-android/pull/13) ([awushensky](https://github.com/awushensky))
- Simplify results [\#12](https://github.com/getbouncer/cardscan-ui-android/pull/12) ([awushensky](https://github.com/awushensky))
## [2.0.0008](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0008) (2020-05-21)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0007...2.0.0008)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0007...2.0.0008)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0007...2.0.0008)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0007...2.0.0008)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0007...2.0.0008)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0007...2.0.0008)
**Merged pull requests:**
- 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))
- 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))
- 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))
- Make dependencies explicit [\#39](https://github.com/getbouncer/cardscan-demo-android/pull/39) ([awushensky](https://github.com/awushensky))
- Display card pan always [\#38](https://github.com/getbouncer/cardscan-demo-android/pull/38) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- Use invalid api key for test [\#14](https://github.com/getbouncer/scan-framework-android/pull/14) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- 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))
- Add more tests [\#8](https://github.com/getbouncer/scan-framework-android/pull/8) ([awushensky](https://github.com/awushensky))
- Add android test github action [\#7](https://github.com/getbouncer/scan-framework-android/pull/7) ([awushensky](https://github.com/awushensky))
- Ensure results are not duplicated [\#6](https://github.com/getbouncer/scan-framework-android/pull/6) ([awushensky](https://github.com/awushensky))
- Optimize some image utilities [\#5](https://github.com/getbouncer/scan-framework-android/pull/5) ([awushensky](https://github.com/awushensky))
- Refocus camera [\#15](https://github.com/getbouncer/scan-camera-android/pull/15) ([awushensky](https://github.com/awushensky))
- Simplify camera start [\#14](https://github.com/getbouncer/scan-camera-android/pull/14) ([awushensky](https://github.com/awushensky))
- Ignore camera config change failures [\#13](https://github.com/getbouncer/scan-camera-android/pull/13) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- 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))
- 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))
- Add tests to camera [\#5](https://github.com/getbouncer/scan-camera-android/pull/5) ([awushensky](https://github.com/awushensky))
- Remove camerax and camera2 [\#4](https://github.com/getbouncer/scan-camera-android/pull/4) ([awushensky](https://github.com/awushensky))
- Add camera1 and camerax [\#3](https://github.com/getbouncer/scan-camera-android/pull/3) ([awushensky](https://github.com/awushensky))
- Use better coroutine testing [\#13](https://github.com/getbouncer/scan-payment-android/pull/13) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- 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))
- 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))
- Add tests [\#5](https://github.com/getbouncer/scan-payment-android/pull/5) ([awushensky](https://github.com/awushensky))
- 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))
- Add tests [\#10](https://github.com/getbouncer/scan-ui-android/pull/10) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- Update default user interface [\#6](https://github.com/getbouncer/scan-ui-android/pull/6) ([awushensky](https://github.com/awushensky))
- 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))
- 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))
- 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))
- Add integration tests to CI [\#6](https://github.com/getbouncer/cardscan-ui-android/pull/6) ([awushensky](https://github.com/awushensky))
- Update user interface [\#5](https://github.com/getbouncer/cardscan-ui-android/pull/5) ([awushensky](https://github.com/awushensky))
## [2.0.0007](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0007) (2020-05-12)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0006...2.0.0007)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0006...2.0.0007)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0006...2.0.0007)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0006...2.0.0007)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.0006...2.0.0007)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/2.0.0005...2.0.0007)
**Merged pull requests:**
- Update documentation [\#37](https://github.com/getbouncer/cardscan-demo-android/pull/37) ([awushensky](https://github.com/awushensky))
- Fix crash on network failure [\#4](https://github.com/getbouncer/scan-framework-android/pull/4) ([awushensky](https://github.com/awushensky))
- Fix crash on camera open failure [\#2](https://github.com/getbouncer/scan-camera-android/pull/2) ([awushensky](https://github.com/awushensky))
- Update submodules [\#4](https://github.com/getbouncer/scan-payment-android/pull/4) ([awushensky](https://github.com/awushensky))
- Update version [\#5](https://github.com/getbouncer/scan-ui-android/pull/5) ([awushensky](https://github.com/awushensky))
- Update submodules [\#4](https://github.com/getbouncer/cardscan-ui-android/pull/4) ([awushensky](https://github.com/awushensky))
- Set api key on warmup [\#3](https://github.com/getbouncer/cardscan-ui-android/pull/3) ([awushensky](https://github.com/awushensky))
## [2.0.0006](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0006) (2020-05-09)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0005...2.0.0006)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/2.0.0005...2.0.0006)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/2.0.0005...2.0.0006)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/2.0.0005...2.0.0006)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/2.0.005...2.0.0006)
**Merged pull requests:**
- Update version [\#36](https://github.com/getbouncer/cardscan-demo-android/pull/36) ([awushensky](https://github.com/awushensky))
- Fix signedUrl failure crash [\#3](https://github.com/getbouncer/scan-framework-android/pull/3) ([awushensky](https://github.com/awushensky))
- update version [\#3](https://github.com/getbouncer/scan-payment-android/pull/3) ([awushensky](https://github.com/awushensky))
- Allow invalid api key error [\#4](https://github.com/getbouncer/scan-ui-android/pull/4) ([awushensky](https://github.com/awushensky))
## [2.0.0005](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0005) (2020-05-08)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0004...2.0.0005)
[Full Changelog](https://github.com/getbouncer/scan-framework-android/compare/e0e5089a945c8b6b6ed47f3838d3d61997c1afe4...2.0.0005)
[Full Changelog](https://github.com/getbouncer/scan-camera-android/compare/70e9702f2bb0a99ef89e1477064eb541b661170f...2.0.0005)
[Full Changelog](https://github.com/getbouncer/scan-payment-android/compare/d19502708ef72c01d522e2d18e7a8bfbb81ec0b2...2.0.0005)
[Full Changelog](https://github.com/getbouncer/scan-ui-android/compare/0904ef185c19491c25b73c61eb8a22bed1eecb75...2.0.005)
[Full Changelog](https://github.com/getbouncer/cardscan-ui-android/compare/ebb299b0e1b799ec7c12aff1f535d0278d9107c1...2.0.0005)
**Merged pull requests:**
- Update submodules [\#35](https://github.com/getbouncer/cardscan-demo-android/pull/35) ([awushensky](https://github.com/awushensky))
- Replace libraries [\#34](https://github.com/getbouncer/cardscan-demo-android/pull/34) ([awushensky](https://github.com/awushensky))
- Remove build from git [\#2](https://github.com/getbouncer/scan-framework-android/pull/2) ([awushensky](https://github.com/awushensky))
- Update documentation [\#1](https://github.com/getbouncer/scan-framework-android/pull/1) ([awushensky](https://github.com/awushensky))
- Update documentation [\#1](https://github.com/getbouncer/scan-camera-android/pull/1) ([awushensky](https://github.com/awushensky))
- Add results [\#2](https://github.com/getbouncer/scan-payment-android/pull/2) ([awushensky](https://github.com/awushensky))
- Update documentation [\#1](https://github.com/getbouncer/scan-payment-android/pull/1) ([awushensky](https://github.com/awushensky))
- Rename module in docs [\#3](https://github.com/getbouncer/scan-ui-android/pull/3) ([awushensky](https://github.com/awushensky))
- Rename module [\#2](https://github.com/getbouncer/scan-ui-android/pull/2) ([awushensky](https://github.com/awushensky))
- Update docs [\#1](https://github.com/getbouncer/scan-ui-android/pull/1) ([awushensky](https://github.com/awushensky))
- Update submodules [\#2](https://github.com/getbouncer/cardscan-ui-android/pull/2) ([awushensky](https://github.com/awushensky))
- Update documentation [\#1](https://github.com/getbouncer/cardscan-ui-android/pull/1) ([awushensky](https://github.com/awushensky))
## [2.0.0004](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0004) (2020-05-06)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/2.0.0003...2.0.0004)
**Merged pull requests:**
- Update version [\#33](https://github.com/getbouncer/cardscan-demo-android/pull/33) ([awushensky](https://github.com/awushensky))
## [2.0.0003](https://github.com/getbouncer/cardscan-demo-android/tree/2.0.0003) (2020-04-29)
[Full Changelog](https://github.com/getbouncer/cardscan-demo-android/compare/a0e3c8e303b7f72e2b08701e01781d9558b07e2c...2.0.0003)
**Implemented enhancements:**
- Update github checks [\#5](https://github.com/getbouncer/cardscan-demo-android/pull/5) ([awushensky](https://github.com/awushensky))
**Merged pull requests:**
- Update thread handling [\#32](https://github.com/getbouncer/cardscan-demo-android/pull/32) ([awushensky](https://github.com/awushensky))
- Update documentation [\#31](https://github.com/getbouncer/cardscan-demo-android/pull/31) ([awushensky](https://github.com/awushensky))
- Extract common ui [\#30](https://github.com/getbouncer/cardscan-demo-android/pull/30) ([awushensky](https://github.com/awushensky))
- Update submodules [\#29](https://github.com/getbouncer/cardscan-demo-android/pull/29) ([awushensky](https://github.com/awushensky))
- Update submodules [\#28](https://github.com/getbouncer/cardscan-demo-android/pull/28) ([awushensky](https://github.com/awushensky))
- Update submodules [\#27](https://github.com/getbouncer/cardscan-demo-android/pull/27) ([awushensky](https://github.com/awushensky))
- Update submodules [\#26](https://github.com/getbouncer/cardscan-demo-android/pull/26) ([awushensky](https://github.com/awushensky))
- Update submodules [\#25](https://github.com/getbouncer/cardscan-demo-android/pull/25) ([awushensky](https://github.com/awushensky))
- Update submodules [\#24](https://github.com/getbouncer/cardscan-demo-android/pull/24) ([awushensky](https://github.com/awushensky))
- Update submodules [\#23](https://github.com/getbouncer/cardscan-demo-android/pull/23) ([awushensky](https://github.com/awushensky))
- Prevent screenshots [\#22](https://github.com/getbouncer/cardscan-demo-android/pull/22) ([awushensky](https://github.com/awushensky))
- Add api key check [\#21](https://github.com/getbouncer/cardscan-demo-android/pull/21) ([awushensky](https://github.com/awushensky))
- Update license [\#20](https://github.com/getbouncer/cardscan-demo-android/pull/20) ([awushensky](https://github.com/awushensky))
- Update documentation [\#19](https://github.com/getbouncer/cardscan-demo-android/pull/19) ([awushensky](https://github.com/awushensky))
- Update submodules [\#18](https://github.com/getbouncer/cardscan-demo-android/pull/18) ([awushensky](https://github.com/awushensky))
- Add documentation [\#17](https://github.com/getbouncer/cardscan-demo-android/pull/17) ([awushensky](https://github.com/awushensky))
- Make logo optional [\#16](https://github.com/getbouncer/cardscan-demo-android/pull/16) ([awushensky](https://github.com/awushensky))
- Update submodules [\#15](https://github.com/getbouncer/cardscan-demo-android/pull/15) ([awushensky](https://github.com/awushensky))
- Update submodules [\#14](https://github.com/getbouncer/cardscan-demo-android/pull/14) ([awushensky](https://github.com/awushensky))
- Rename app to demo [\#13](https://github.com/getbouncer/cardscan-demo-android/pull/13) ([awushensky](https://github.com/awushensky))
- Support state in loops [\#12](https://github.com/getbouncer/cardscan-demo-android/pull/12) ([awushensky](https://github.com/awushensky))
- Remove camera 1 api [\#11](https://github.com/getbouncer/cardscan-demo-android/pull/11) ([awushensky](https://github.com/awushensky))
- Scope tests to the app itself [\#10](https://github.com/getbouncer/cardscan-demo-android/pull/10) ([awushensky](https://github.com/awushensky))
- Update submodules [\#9](https://github.com/getbouncer/cardscan-demo-android/pull/9) ([awushensky](https://github.com/awushensky))
- Update submodules [\#8](https://github.com/getbouncer/cardscan-demo-android/pull/8) ([awushensky](https://github.com/awushensky))
- Use submodules [\#7](https://github.com/getbouncer/cardscan-demo-android/pull/7) ([awushensky](https://github.com/awushensky))
- Show the card pan when scanninng [\#6](https://github.com/getbouncer/cardscan-demo-android/pull/6) ([awushensky](https://github.com/awushensky))
- Update dependencies [\#4](https://github.com/getbouncer/cardscan-demo-android/pull/4) ([awushensky](https://github.com/awushensky))
- Require API key [\#3](https://github.com/getbouncer/cardscan-demo-android/pull/3) ([awushensky](https://github.com/awushensky))
- Add code owners [\#2](https://github.com/getbouncer/cardscan-demo-android/pull/2) ([awushensky](https://github.com/awushensky))
- Support camera1 APIs [\#1](https://github.com/getbouncer/cardscan-demo-android/pull/1) ([awushensky](https://github.com/awushensky))
================================================
FILE: LICENSE
================================================
The MIT License
Copyright (c) 2011- Stripe, Inc. (https://stripe.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# Deprecation Notice
Hello from the Stripe (formerly Bouncer) team!
We'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.
This 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!
If 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.
If 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).
For the new product, please visit the [stripe github repository](https://github.com/stripe/stripe-android/tree/master/stripecardscan).
# Overview
This 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.
[CardScan](https://getbouncer.com/scan) is a relatively small library that provides fast and accurate payment card scanning.
CardScan is the foundation for CardVerify enterprise libraries, which validate the authenticity of payment cards as they are scanned.




[](https://github.com/getbouncer/cardscan-android/releases)

## Contents
* [Requirements](#requirements)
* [Demo](#demo)
* [Integration](#integration)
* [Customizing](#customizing)
* [Developing](#developing)
* [Authors](#authors)
* [License](#license)
## Requirements
* Android API level 21 or higher
* AndroidX compatibility
* Kotlin coroutine compatibility
Note: Your app does not have to be written in kotlin to integrate this library, but must be able to depend on kotlin functionality.
## Demo
This 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).
## Integration
See the [integration documentation](https://docs.getbouncer.com/card-scan/android-integration-guide) in the Bouncer Docs.
### Provisioning an API key
CardScan requires a valid API key to run. To provision an API key, visit the [Bouncer API console](https://api.getbouncer.com/console).
### Name and expiration extraction support (BETA)
To 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.
Before launching the CardScan flow, make sure to call the ```CardScanActivity.warmup()``` function with your API key and set ```initializeNameAndExpiryExtraction``` to ```true```
```kotlin
CardScanActivity.warmup(this, API_KEY, true)
```
## Customizing
CardScan is built to be customized to fit your UI.
### Basic modifications
To modify text, colors, or padding of the default UI, see the [customization](https://docs.getbouncer.com/card-scan/android-integration-guide/customization-guide) documentation.
### Extensive modifications
To 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).
## Developing
See the [development docs](https://docs.getbouncer.com/card-scan/android-integration-guide/development-guide) for details on developing for CardScan.
## Authors
Adam Wushensky, Sam King, and Zain ul Abi Din
## License
This library is available under the MIT license. See the [LICENSE](LICENSE) file for the full license text.
================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
mavenLocal()
mavenCentral()
google()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
plugins {
id 'org.jetbrains.kotlin.plugin.serialization' version "1.5.31"
id 'org.jetbrains.dokka' version '1.5.0'
id "io.github.gradle-nexus.publish-plugin" version "1.1.0"
}
allprojects {
apply plugin: 'checkstyle'
checkstyle {
toolVersion '8.29'
}
configurations {
javadocDeps
kotlinlint
}
dependencies {
kotlinlint("com.pinterest:ktlint:0.41.0") {
attributes {
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
}
}
}
repositories {
mavenLocal()
mavenCentral()
google()
maven { url "https://plugins.gradle.org/m2/" }
}
task checkJavaStyle(type: Checkstyle) {
showViolations = true
configFile file("../settings/checkstyle.xml")
source 'src/main/java'
include '**/*.java'
exclude '**/gen/**'
exclude '**/R.java'
exclude '**/BuildConfig.java'
// empty classpath
classpath = files()
}
task ktlint(type: JavaExec, group: "verification") {
description = "Check Kotlin code style."
main = "com.pinterest.ktlint.Main"
classpath = configurations.kotlinlint
args "src/**/*.kt"
// to generate report in checkstyle format prepend following args:
// "--reporter=plain", "--reporter=checkstyle,output=${buildDir}/ktlint.xml"
// see https://github.com/pinterest/ktlint#usage for more
}
task ktlintFormat(type: JavaExec, group: "formatting") {
description = "Fix Kotlin code style deviations."
main = "com.pinterest.ktlint.Main"
classpath = configurations.kotlinlint
args "-F", "src/**/*.kt"
}
}
ext {
publishGroupId = 'com.getbouncer'
}
File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
Properties p = new Properties()
new FileInputStream(secretPropsFile).withCloseable { is ->
p.load(is)
}
p.each { name, value ->
ext[name] = value
}
} else {
ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')
ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
}
nexusPublishing {
repositories {
sonatype {
nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
packageGroup = publishGroupId
stagingProfileId = sonatypeStagingProfileId
username = ossrhUsername
password = ossrhPassword
}
}
}
================================================
FILE: cardscan-demo/.gitignore
================================================
/build
================================================
FILE: cardscan-demo/README.md
================================================
# Deprecation Notice
Hello from the Stripe (formerly Bouncer) team!
We'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.
This 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!
If 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.
If 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).
For the new product, please visit the [stripe github repository](https://github.com/stripe/stripe-android/tree/master/stripecardscan).
# Overview
This 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.
CardScan is the foundation for CardVerify enterprise libraries, which validate the authenticity of payment cards as they are scanned.

## Contents
* [Requirements](#requirements)
* [Demo](#demo)
* [Integration](#integration)
* [Customizing](#customizing)
* [Developing](#developing)
* [Authors](#authors)
* [License](#license)
## Requirements
* Android API level 21 or higher
* AndroidX compatibility
* Kotlin coroutine compatibility
Note: Your app does not have to be written in kotlin to integrate this library, but must be able to depend on kotlin functionality.
## Demo
This repository contains a demonstration app for the CardScan product. To build and install this library follow the following steps:
1. Clone the repository from github
```bash
git clone --recursive https://github.com/getbouncer/cardscan-demo-android
```
2. Build the library using gradle or [android studio](https://developer.android.com/studio).
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.

b. Using gradle, build the demo app by executing the following command:
```bash
./gradlew demo:assembleRelease
```
This will create a release APK in the `cardscan-demo/build/outputs/apk` directory. Copy this file to your device and install it.
## Integration
See the [integration documentation](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) in the Bouncer Docs.
### Provisioning an API key
CardScan requires a valid API key to run. To provision an API key, visit the [Bouncer API console](https://api.getbouncer.com/console).
### Name and expiration extraction support (BETA)
To 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.
Before launching the CardScan flow, make sure to call the ```CardScanActivity.warmup()``` function with your API key and set ```initializeNameAndExpiryExtraction``` to ```true```
```kotlin
CardScanActivity.warmup(this, API_KEY, true)
```
## Customizing
CardScan is built to be customized to fit your UI.
### Basic modifications
To 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.
### Extensive modifications
To 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).
## Developing
See the [development docs](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) for details on developing for CardScan.
## Authors
Adam Wushensky, Sam King, and Zain ul Abi Din
## License
This library is available under the MIT license. See the [LICENSE](../LICENSE) file for the full license text.
================================================
FILE: cardscan-demo/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 30
buildToolsVersion '30.0.3'
defaultConfig {
applicationId "com.getbouncer.cardscan.demo"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
lintOptions {
enable "Interoperability"
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":scan-camerax")
implementation project(':scan-payment-full')
implementation project(":cardscan-ui")
implementation "androidx.appcompat:appcompat:1.3.1"
implementation "androidx.core:core-ktx:1.6.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1"
}
dependencies {
testImplementation "androidx.test:core:1.4.0"
testImplementation "androidx.test:runner:1.4.0"
testImplementation "junit:junit:4.13.2"
testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30"
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
}
dependencies {
androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30"
}
================================================
FILE: cardscan-demo/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: cardscan-demo/src/androidTest/java/com/getbouncer/cardscan/demo/ExampleInstrumentedTest.kt
================================================
package com.getbouncer.cardscan.demo
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Test
import kotlin.test.assertEquals
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.getbouncer.cardscan.demo", appContext.packageName)
}
}
================================================
FILE: cardscan-demo/src/main/AndroidManifest.xml
================================================
================================================
FILE: cardscan-demo/src/main/java/com/getbouncer/cardscan/demo/LaunchActivity.java
================================================
package com.getbouncer.cardscan.demo;
import android.app.AlertDialog;
import android.content.Intent;
import android.os.Bundle;
import android.view.WindowManager;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.getbouncer.cardscan.ui.CardScanSheet;
import com.getbouncer.cardscan.ui.CardScanSheetResult;
import com.getbouncer.cardscan.ui.ScannedCard;
import com.getbouncer.scan.framework.Config;
import com.getbouncer.scan.framework.Scan;
import com.getbouncer.scan.ui.CancellationReason;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import kotlin.Unit;
public class LaunchActivity extends AppCompatActivity {
private static final String API_KEY = "qOJ_fF-WLDMbG05iBq5wvwiTNTmM2qIn";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_launch);
final CardScanSheet sheet = CardScanSheet.create(this, API_KEY, this::handleScanResult);
// Because this activity displays card numbers, disallow screenshots.
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
);
((CheckBox) findViewById(R.id.enableDebugCheckbox))
.setOnCheckedChangeListener((buttonView, isChecked) -> Config.setDebug(isChecked));
findViewById(R.id.scanCardButton).setOnClickListener(v -> {
final boolean enableNameExtraction =
((CheckBox) findViewById(R.id.enableNameExtractionCheckbox)).isChecked();
final boolean enableExpiryExtraction =
((CheckBox) findViewById(R.id.enableExpiryExtractionCheckbox)).isChecked();
final boolean enableEnterCardManually =
((CheckBox) findViewById(R.id.enableEnterCardManuallyCheckbox)).isChecked();
sheet.present(
/* enableEnterCardManually */ enableEnterCardManually,
/* enableExpiryExtraction */ enableExpiryExtraction,
/* enableNameExtraction */ enableNameExtraction
);
});
if (Scan.INSTANCE.isDeviceArchitectureArm()) {
((TextView) findViewById(R.id.deviceArchitectureText))
.setText(getString(
R.string.deviceArchitecture,
"arm: " + Scan.INSTANCE.getDeviceArchitecture()
));
} else {
((TextView) findViewById(R.id.deviceArchitectureText))
.setText(getString(
R.string.deviceArchitecture,
"NOT arm" + Scan.INSTANCE.getDeviceArchitecture()
));
}
findViewById(R.id.singleActivityDemo).setOnClickListener(v ->
startActivity(new Intent(this, SingleActivityDemo.class))
);
CardScanSheet.prepareScan(this, API_KEY, true, () -> null);
}
private Unit handleScanResult(final CardScanSheetResult result) {
if (result instanceof CardScanSheetResult.Completed) {
cardScanned(((CardScanSheetResult.Completed) result).getScannedCard());
} else if (result instanceof CardScanSheetResult.Canceled) {
userCanceled(((CardScanSheetResult.Canceled) result).getReason());
} else if (result instanceof CardScanSheetResult.Failed) {
analyzerFailure(((CardScanSheetResult.Failed) result).getError());
}
return Unit.INSTANCE;
}
private void cardScanned(@NotNull final ScannedCard scanResult) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
StringBuilder message = new StringBuilder();
message.append(scanResult.getPan());
if (scanResult.getCardholderName() != null) {
message.append("\nName: ");
message.append(scanResult.getCardholderName());
}
if (scanResult.getExpiryMonth() != null && scanResult.getExpiryYear() != null) {
message.append(
String.format("\nExpiry: %s/%s",
scanResult.getExpiryMonth(),
scanResult.getExpiryYear())
);
}
if (scanResult.getErrorString() != null) {
message.append("\nError: ");
message.append(scanResult.getErrorString());
}
builder.setMessage(message);
builder.show();
}
private void userCanceled(@NotNull final CancellationReason reason) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
if (reason instanceof CancellationReason.Back) {
builder.setMessage(R.string.user_pressed_back);
} else if (reason instanceof CancellationReason.Closed) {
builder.setMessage(R.string.scan_canceled);
} else if (reason instanceof CancellationReason.CameraPermissionDenied) {
builder.setMessage(R.string.permission_denied);
} else if (reason instanceof CancellationReason.UserCannotScan) {
builder.setMessage(R.string.enter_manually);
}
builder.show();
}
private void analyzerFailure(@NotNull final Throwable reason) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(reason.getMessage());
builder.show();
}
}
================================================
FILE: cardscan-demo/src/main/java/com/getbouncer/cardscan/demo/SingleActivityDemo.java
================================================
package com.getbouncer.cardscan.demo;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.PointF;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.util.Size;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.getbouncer.cardscan.ui.CardScanFlow;
import com.getbouncer.cardscan.ui.SavedFrame;
import com.getbouncer.cardscan.ui.analyzer.CompletionLoopAnalyzer;
import com.getbouncer.cardscan.ui.result.CompletionLoopListener;
import com.getbouncer.cardscan.ui.result.CompletionLoopResult;
import com.getbouncer.cardscan.ui.result.MainLoopAggregator;
import com.getbouncer.cardscan.ui.result.MainLoopState;
import com.getbouncer.scan.camera.CameraAdapter;
import com.getbouncer.scan.camera.CameraErrorListener;
import com.getbouncer.scan.camera.CameraPreviewImage;
import com.getbouncer.scan.camera.CameraSelectorKt;
import com.getbouncer.scan.framework.AggregateResultListener;
import com.getbouncer.scan.framework.AnalyzerLoopErrorListener;
import com.getbouncer.scan.framework.Config;
import com.getbouncer.scan.framework.Stats;
import com.getbouncer.scan.framework.api.BouncerApi;
import com.getbouncer.scan.framework.api.dto.ScanStatistics;
import com.getbouncer.scan.framework.interop.BlockingAggregateResultListener;
import com.getbouncer.scan.framework.util.AppDetails;
import com.getbouncer.scan.framework.util.Device;
import com.getbouncer.scan.payment.card.CardExpiryKt;
import com.getbouncer.scan.payment.card.PanFormatterKt;
import com.getbouncer.scan.payment.card.PaymentCardUtils;
import com.getbouncer.scan.ui.ViewFinderBackground;
import com.getbouncer.scan.ui.util.ViewExtensionsKt;
import java.util.Locale;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import kotlin.Unit;
import kotlin.coroutines.CoroutineContext;
import kotlinx.coroutines.CoroutineScope;
import kotlinx.coroutines.Dispatchers;
public class SingleActivityDemo extends AppCompatActivity implements CameraErrorListener,
AnalyzerLoopErrorListener, CoroutineScope {
private enum State {
NOT_FOUND,
FOUND,
CORRECT
}
private static final int PERMISSION_REQUEST_CODE = 1200;
private static final Size MINIMUM_RESOLUTION = new Size(1280, 720);
private Button scanCardButton;
private View scanView;
private FrameLayout cameraPreview;
private FrameLayout viewFinderWindow;
private ViewFinderBackground viewFinderBackground;
private ImageView viewFinderBorder;
private View processingOverlay;
private ImageView flashButtonView;
private TextView cardPanTextView;
private CameraAdapter> cameraAdapter;
private CardScanFlow cardScanFlow;
private State scanState = State.NOT_FOUND;
private String pan = null;
/**
* CardScan uses kotlin coroutines to run multiple analyzers in parallel for maximum image
* throughput. This coroutine context binds the coroutines to this activity, so that if this
* activity is terminated, all coroutines are terminated and there is no work leak.
*
* Additionally, this specifies which threads the coroutines will run on. Normally, the default
* dispatchers should be used so that coroutines run on threads bound by the number of CPU
* cores.
*/
@NotNull
@Override
public CoroutineContext getCoroutineContext() {
return Dispatchers.getDefault();
}
@Override
@SuppressLint("ClickableViewAccessibility")
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_demo);
scanCardButton = findViewById(R.id.scanCardButton);
scanView = findViewById(R.id.scanView);
scanCardButton.setOnClickListener(v -> {
scanCardButton.setVisibility(View.GONE);
scanView.setVisibility(View.VISIBLE);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) !=
PackageManager.PERMISSION_GRANTED) {
requestCameraPermission();
} else {
startScan();
}
});
cameraPreview = findViewById(R.id.cameraPreviewHolder);
viewFinderWindow = findViewById(R.id.viewFinderWindow);
viewFinderBackground = findViewById(R.id.viewFinderBackground);
viewFinderBorder = findViewById(R.id.viewFinderBorder);
processingOverlay = findViewById(R.id.processing_overlay);
flashButtonView = findViewById(R.id.flashButtonView);
ImageView closeButtonView = findViewById(R.id.closeButtonView);
cardPanTextView = findViewById(R.id.cardPanTextView);
closeButtonView.setOnClickListener(v -> userCancelScan());
flashButtonView.setOnClickListener(v -> setFlashlightState(!cameraAdapter.isTorchOn()));
// Allow the user to set the focus of the camera by tapping on the view finder.
viewFinderWindow.setOnTouchListener((v, event) -> {
cameraAdapter.setFocus(new PointF(
event.getX() + viewFinderWindow.getLeft(),
event.getY() + viewFinderWindow.getTop())
);
return true;
});
}
@Override
protected void onDestroy() {
super.onDestroy();
if (cardScanFlow != null) {
cardScanFlow.cancelFlow();
}
}
@Override
protected void onPause() {
super.onPause();
setFlashlightState(false);
}
@Override
protected void onResume() {
super.onResume();
setStateNotFound();
}
/**
* Request permission to use the camera.
*/
private void requestCameraPermission() {
ActivityCompat.requestPermissions(
this,
new String[] { Manifest.permission.CAMERA },
PERMISSION_REQUEST_CODE
);
}
/**
* Handle permission status changes. If the camera permission has been granted, start it. If
* not, show a dialog.
*/
@Override
public void onRequestPermissionsResult(
int requestCode,
@NotNull String[] permissions,
@NotNull int[] grantResults
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE && grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startScan();
} else {
showPermissionDeniedDialog();
}
}
}
/**
* Show an explanation dialog for why we are requesting camera permissions.
*/
private void showPermissionDeniedDialog() {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.bouncer_camera_permission_denied_message)
.setPositiveButton(
R.string.bouncer_camera_permission_denied_ok,
(dialog, which) -> requestCameraPermission()
)
.setNegativeButton(
R.string.bouncer_camera_permission_denied_cancel,
(dialog, which) -> startScan()
)
.show();
}
/**
* Start the scanning flow.
*/
private void startScan() {
// ensure the cameraPreview view has rendered.
cameraPreview.post(() -> {
// Track scan statistics for health check
Stats.INSTANCE.startScan();
// Tell the background where to draw a hole for the viewfinder window
viewFinderBackground.setViewFinderRect(ViewExtensionsKt.asRect(viewFinderWindow));
// Create a camera adapter and bind it to this activity.
cameraAdapter = CameraSelectorKt.getCameraAdapter(
this,
cameraPreview,
MINIMUM_RESOLUTION,
this
);
cameraAdapter.bindToLifecycle(this);
cameraAdapter.withFlashSupport(supported -> {
flashButtonView.setVisibility(supported ? View.VISIBLE : View.INVISIBLE);
return Unit.INSTANCE;
});
// Create and start a CardScanFlow which will handle the business logic of the scan
cardScanFlow = new CardScanFlow(
true,
true,
aggregateResultListener,
this
);
cardScanFlow.startFlow(
this,
cameraAdapter.getImageStream(),
ViewExtensionsKt.asRect(viewFinderWindow),
this,
this
);
});
}
/**
* Turn the flashlight on or off.
*/
private void setFlashlightState(boolean on) {
if (cameraAdapter != null) {
cameraAdapter.setTorchState(on);
if (cameraAdapter.isTorchOn()) {
flashButtonView.setImageResource(R.drawable.bouncer_flash_on_dark);
} else {
flashButtonView.setImageResource(R.drawable.bouncer_flash_off_dark);
}
}
}
/**
* Cancel scanning due to analyzer failure
*/
private void analyzerFailureCancelScan(@Nullable final Throwable cause) {
Log.e(Config.getLogTag(), "Canceling scan due to analyzer error", cause);
new AlertDialog.Builder(this)
.setMessage("Analyzer failure")
.show();
closeScanner();
}
/**
* Cancel scanning due to a camera error.
*/
private void cameraErrorCancelScan(@Nullable final Throwable cause) {
Log.e(Config.getLogTag(), "Canceling scan due to camera error", cause);
new AlertDialog.Builder(this)
.setMessage("Camera error")
.show();
closeScanner();
}
/**
* The scan has been cancelled by the user.
*/
private void userCancelScan() {
new AlertDialog.Builder(this)
.setMessage("Scan Canceled by user")
.show();
closeScanner();
}
/**
* Show the completed scan results
*/
private void completeScan(
@Nullable String expiryMonth,
@Nullable String expiryYear,
@Nullable String cardNumber,
@Nullable String issuer,
@Nullable String name,
@Nullable String error
) {
new AlertDialog.Builder(this)
.setMessage(String.format(
Locale.getDefault(),
"%s\n%s\n%s/%s\n%s\n%s",
cardNumber,
issuer,
expiryMonth,
expiryYear,
name,
error
))
.show();
closeScanner();
}
/**
* Close the scanner.
*/
private void closeScanner() {
setFlashlightState(false);
scanCardButton.setVisibility(View.VISIBLE);
scanView.setVisibility(View.GONE);
setStateNotFound();
cameraAdapter.unbindFromLifecycle(this);
if (cardScanFlow != null) {
cardScanFlow.cancelFlow();
}
BouncerApi.uploadScanStats(
this,
Stats.INSTANCE.getInstanceId(),
Stats.INSTANCE.getScanId(),
Device.fromContext(this),
AppDetails.fromContext(this),
ScanStatistics.fromStats()
);
}
@Override
public void onCameraOpenError(@Nullable Throwable cause) {
cameraErrorCancelScan(cause);
}
@Override
public void onCameraAccessError(@Nullable Throwable cause) {
cameraErrorCancelScan(cause);
}
@Override
public void onCameraUnsupportedError(@Nullable Throwable cause) {
cameraErrorCancelScan(cause);
}
@Override
public boolean onAnalyzerFailure(@NotNull Throwable t) {
analyzerFailureCancelScan(t);
return true;
}
@Override
public boolean onResultFailure(@NotNull Throwable t) {
analyzerFailureCancelScan(t);
return true;
}
private final CompletionLoopListener completionLoopListener = new CompletionLoopListener() {
@Override
public void onCompletionLoopFrameProcessed(
@NotNull CompletionLoopAnalyzer.Prediction result,
@NotNull SavedFrame frame
) {
// display debug information if so desired
}
@Override
public void onCompletionLoopDone(@NotNull CompletionLoopResult result) {
@Nullable final String expiryMonth;
@Nullable final String expiryYear;
if (result.getExpiryMonth() != null &&
result.getExpiryYear() != null &&
CardExpiryKt.isValidExpiry(
null,
result.getExpiryMonth(),
result.getExpiryYear()
)
) {
expiryMonth = result.getExpiryMonth();
expiryYear = result.getExpiryYear();
} else {
expiryMonth = null;
expiryYear = null;
}
new Handler(getMainLooper()).post(() -> {
// Only show the expiry dates that are not expired
completeScan(
expiryMonth,
expiryYear,
SingleActivityDemo.this.pan,
PaymentCardUtils.getCardIssuer(SingleActivityDemo.this.pan).getDisplayName(),
result.getName(),
result.getErrorString()
);
});
}
};
private final AggregateResultListener<
MainLoopAggregator.InterimResult,
MainLoopAggregator.FinalResult> aggregateResultListener =
new BlockingAggregateResultListener<
MainLoopAggregator.InterimResult,
MainLoopAggregator.FinalResult>() {
/**
* An interim result has been received from the scan, the scan is still running. Update your
* UI as necessary here to display the progress of the scan.
*/
@Override
public void onInterimResultBlocking(MainLoopAggregator.InterimResult interimResult) {
new Handler(getMainLooper()).post(() -> {
final MainLoopState mainLoopState = interimResult.getState();
if (mainLoopState instanceof MainLoopState.Initial) {
// In initial state, show no card found
setStateNotFound();
} else if (mainLoopState instanceof MainLoopState.PanFound) {
// If OCR is running and a valid card number is visible, display it
final MainLoopState.PanFound state = (MainLoopState.PanFound) mainLoopState;
final String pan = state.getMostLikelyPan();
if (pan != null) {
cardPanTextView.setText(PanFormatterKt.formatPan(pan));
ViewExtensionsKt.show(cardPanTextView);
}
setStateFound();
} else if (mainLoopState instanceof MainLoopState.CardSatisfied) {
// If OCR is running and a valid card number is visible, display it
final MainLoopState.CardSatisfied state =
(MainLoopState.CardSatisfied) mainLoopState;
final String pan = state.getMostLikelyPan();
if (pan != null) {
cardPanTextView.setText(PanFormatterKt.formatPan(pan));
ViewExtensionsKt.show(cardPanTextView);
}
setStateFound();
} else if (mainLoopState instanceof MainLoopState.PanSatisfied) {
// If OCR is running and a valid card number is visible, display it
final MainLoopState.PanSatisfied state =
(MainLoopState.PanSatisfied) mainLoopState;
final String pan = state.getPan();
if (pan != null) {
cardPanTextView.setText(PanFormatterKt.formatPan(pan));
ViewExtensionsKt.show(cardPanTextView);
}
setStateFound();
} else if (mainLoopState instanceof MainLoopState.Finished) {
// Once the main loop has finished, the camera can stop
cameraAdapter.unbindFromLifecycle(SingleActivityDemo.this);
setStateCorrect();
}
});
}
/**
* The scan has completed and the final result is available. Close the scanner and make use
* of the final result.
*/
@Override
public void onResultBlocking(MainLoopAggregator.FinalResult result) {
SingleActivityDemo.this.pan = result.getPan();
cardScanFlow.launchCompletionLoop(
SingleActivityDemo.this,
completionLoopListener,
cardScanFlow.selectCompletionLoopFrames(
result.getAverageFrameRate(),
result.getSavedFrames()
),
result.getAverageFrameRate().compareTo(Config.getSlowDeviceFrameRate()) > 0,
SingleActivityDemo.this
);
}
/**
* The scan was reset (usually because the activity was backgrounded). Reset the UI.
*/
@Override
public void onResetBlocking() {
new Handler(getMainLooper()).post(() -> setStateNotFound());
}
};
/**
* Display a blue border tracing the outline of the card to indicate that the card is identified
* and scanning is running.
*/
private void setStateFound() {
if (scanState == State.FOUND) return;
ViewExtensionsKt.startAnimation(viewFinderBorder,
R.drawable.bouncer_card_border_found_long);
ViewExtensionsKt.hide(processingOverlay);
scanState = State.FOUND;
}
/**
* Return the view to its initial state, where no card has been detected.
*/
private void setStateNotFound() {
if (scanState == State.NOT_FOUND) return;
ViewExtensionsKt.startAnimation(viewFinderBorder, R.drawable.bouncer_card_border_not_found);
ViewExtensionsKt.hide(cardPanTextView);
ViewExtensionsKt.hide(processingOverlay);
scanState = State.NOT_FOUND;
}
/**
* Flash the border around the card green to indicate that scanning was successful.
*/
private void setStateCorrect() {
if (scanState == State.CORRECT) return;
ViewExtensionsKt.startAnimation(viewFinderBorder, R.drawable.bouncer_card_border_correct);
ViewExtensionsKt.show(processingOverlay);
scanState = State.CORRECT;
}
}
================================================
FILE: cardscan-demo/src/main/res/drawable/ic_launcher_background.xml
================================================
================================================
FILE: cardscan-demo/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
================================================
FILE: cardscan-demo/src/main/res/layout/activity_launch.xml
================================================
================================================
FILE: cardscan-demo/src/main/res/layout/activity_single_demo.xml
================================================
================================================
FILE: cardscan-demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: cardscan-demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: cardscan-demo/src/main/res/values/colors.xml
================================================
#008577#00574B#D81B60
================================================
FILE: cardscan-demo/src/main/res/values/strings.xml
================================================
CardScan DemoScan CardUser pressed backScan canceledEnter card manuallyCamera permission deniedSingle Activity DemoEnable DebugEnable name extractionEnable expiry extractionEnable manual entryDevice architecture is %1$s
================================================
FILE: cardscan-demo/src/main/res/values/styles.xml
================================================
================================================
FILE: cardscan-demo/src/test/java/com/getbouncer/cardscan/demo/ExampleUnitTest.kt
================================================
package com.getbouncer.cardscan.demo
import org.junit.Test
import kotlin.test.assertEquals
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
================================================
FILE: cardscan-ui/.gitignore
================================================
/build
================================================
FILE: cardscan-ui/README.md
================================================
# Deprecation Notice
Hello from the Stripe (formerly Bouncer) team!
We'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.
This 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!
If 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.
If 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).
For the new product, please visit the [stripe github repository](https://github.com/stripe/stripe-android/tree/master/stripecardscan).
# Overview
This 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.
This library is the foundation for CardScan and CardVerify enterprise libraries, which validate the authenticity of payment cards as they are scanned.

## Contents
* [Requirements](#requirements)
* [Demo](#demo)
* [Integration](#integration)
* [Customizing](#customizing)
* [Developing](#developing)
* [Authors](#authors)
* [License](#license)
## Requirements
* Android API level 21 or higher
* AndroidX compatibility
* Kotlin coroutine compatibility
Note: Your app does not have to be written in kotlin to integrate this library, but must be able to depend on kotlin functionality.
## Demo
An app demonstrating the basic capabilities of this library is available in [github](https://github.com/getbouncer/cardscan-demo-android).
## Integration
See the [integration documentation](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) in the Bouncer Docs.
### Provisioning an API key
CardScan requires a valid API key to run. To provision an API key, visit the [Bouncer API console](https://api.getbouncer.com/console).
### Name and expiration extraction support (BETA)
To 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.
Before launching the CardScan flow, make sure to call the ```CardScanActivity.warmup()``` function with your API key and set ```initializeNameAndExpiryExtraction``` to ```true```
```kotlin
CardScanActivity.warmup(this, API_KEY, true)
```
## Customizing
CardScan is built to be customized to fit your UI.
### Basic modifications
To 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.
### Extensive modifications
To 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).
## Developing
See the [development docs](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) for details on developing for CardScan.
## Authors
Adam Wushensky, Sam King, and Zain ul Abi Din
## License
This library is available under the MIT license. See the [LICENSE](../LICENSE) file for the full license text.
================================================
FILE: cardscan-ui/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
android {
compileSdkVersion 30
buildToolsVersion '30.0.3'
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
testOptions {
unitTests.includeAndroidResources = true
}
lintOptions {
enable "Interoperability"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api project(":scan-framework")
api project(':scan-camera')
api project(":scan-payment")
api project(":scan-ui")
implementation "androidx.appcompat:appcompat:[1.3.0,1.3.1]"
implementation "androidx.core:core-ktx:[1.3.1,1.6.0]"
implementation 'androidx.constraintlayout:constraintlayout:[2.0.4,2.1.0]'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:[1.4.0,1.5.1]"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:[1.1.0,1.2.2]"
}
dependencies {
testImplementation "androidx.test:core:1.4.0"
testImplementation "androidx.test:runner:1.4.0"
testImplementation "junit:junit:4.13.2"
testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1"
}
dependencies {
androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30"
}
apply from: 'deploy.gradle'
================================================
FILE: cardscan-ui/consumer-rules.pro
================================================
================================================
FILE: cardscan-ui/deploy.gradle
================================================
apply plugin: 'maven-publish'
apply plugin: 'org.jetbrains.dokka'
apply plugin: 'signing'
task androidSourcesJar(type: Jar) {
archiveClassifier.set('sources')
if (project.plugins.findPlugin("com.android.library")) {
// Android library
from android.sourceSets.main.java.srcDirs
from android.sourceSets.main.kotlin.srcDirs
} else {
// Pure kotlin library
from sourceSets.main.java.srcDirs
from sourceSets.main.kotlin.srcDirs
}
}
tasks.withType(dokkaHtmlPartial.getClass()).configureEach {
pluginsMapConfiguration.set(
["org.jetbrains.dokka.base.DokkaBase": """{ "separateInheritedMembers": true}"""]
)
}
task javadocJar(type: Jar, dependsOn: dokkaJavadoc) {
archiveClassifier.set('javadoc')
from dokkaJavadoc.outputDirectory
}
artifacts {
archives androidSourcesJar
archives javadocJar
}
ext["signing.keyId"] = ''
ext["signing.password"] = ''
ext["signing.secretKeyRingFile"] = ''
ext["ossrhUsername"] = ''
ext["ossrhPassword"] = ''
ext["sonatypeStagingProfileId"] = ''
ext {
libraryDescription = 'This library provides the user interface for scanning'
siteUrl = 'https://getbouncer.com'
scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'
scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'
scmUrl = 'https://github.com/getbouncer/cardscan-android'
licenseName = 'bouncer-free-1'
licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'
developerId = 'getbouncer'
developerName = 'Bouncer Technologies'
developerEmail = 'bouncer-support@stripe.com'
publishGroupId = 'com.getbouncer'
publishArtifactId = 'cardscan-ui'
publishVersion = version
}
group = publishGroupId
version = publishVersion
File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
Properties p = new Properties()
new FileInputStream(secretPropsFile).withCloseable { is ->
p.load(is)
}
p.each { name, value ->
ext[name] = value
}
} else {
ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')
ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
}
publishing {
publications {
release(MavenPublication) {
groupId publishGroupId
artifactId publishArtifactId
version publishVersion
// Two artifacts, the `aar` (or `jar`) and the sources
if (project.plugins.findPlugin("com.android.library")) {
artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
} else {
artifact("$buildDir/libs/${project.getName()}-${version}.jar")
}
artifact androidSourcesJar
pom {
name = publishArtifactId
description = libraryDescription
url = siteUrl
licenses {
license {
name = licenseName
url = licenseUrl
}
}
developers {
developer {
id = developerId
name = developerName
email = developerEmail
}
}
scm {
connection = scmConnection
developerConnection = scmDeveloperConnection
url = scmUrl
}
// A slightly hacky fix so that your POM will include any transitive dependencies
// that your library builds upon
withXml {
def dependenciesNode = asNode().appendNode('dependencies')
project.configurations.implementation.allDependencies.each {
if (it.group != null && it.version != null) {
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', it.group)
dependencyNode.appendNode('artifactId', it.name)
dependencyNode.appendNode('version', it.version)
}
}
}
}
}
}
// The repository to publish to, Sonatype/MavenCentral
repositories {
maven {
// This is an arbitrary name, you may also use "mavencentral" or
// any other name that's descriptive for you
name = "sonatype"
url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
credentials {
username ossrhUsername
password ossrhPassword
}
}
}
}
signing {
sign publishing.publications
}
================================================
FILE: cardscan-ui/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: cardscan-ui/src/androidTest/java/com/getbouncer/cardscan/ui/ExampleInstrumentedTest.kt
================================================
package com.getbouncer.cardscan.ui
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Test
import kotlin.test.assertEquals
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.getbouncer.cardscan.ui.test", appContext.packageName)
}
}
================================================
FILE: cardscan-ui/src/main/AndroidManifest.xml
================================================
================================================
FILE: cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/CardScanActivity.kt
================================================
package com.getbouncer.cardscan.ui
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.Gravity
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import com.getbouncer.cardscan.ui.analyzer.CompletionLoopAnalyzer
import com.getbouncer.cardscan.ui.exception.UnknownScanException
import com.getbouncer.cardscan.ui.result.CompletionLoopListener
import com.getbouncer.cardscan.ui.result.CompletionLoopResult
import com.getbouncer.cardscan.ui.result.MainLoopAggregator
import com.getbouncer.scan.framework.AggregateResultListener
import com.getbouncer.scan.framework.AnalyzerLoopErrorListener
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.payment.card.getCardIssuer
import com.getbouncer.scan.payment.card.isValidExpiry
import com.getbouncer.scan.payment.cropCameraPreviewToSquare
import com.getbouncer.scan.ui.CancellationReason
import com.getbouncer.scan.ui.util.getColorByRes
import com.getbouncer.scan.ui.util.hide
import com.getbouncer.scan.ui.util.setTextSizeByRes
import com.getbouncer.scan.ui.util.setVisible
import com.getbouncer.scan.ui.util.show
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@Parcelize
@Deprecated("Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class ScannedCard(
val pan: String?,
val expiryDay: String?,
val expiryMonth: String?,
val expiryYear: String?,
val networkName: String?,
val cvc: String?,
val cardholderName: String?,
val errorString: String?,
) : Parcelable
@Deprecated("Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
interface CardProcessedResultListener : CardScanResultListener {
/**
* A payment card was successfully scanned.
*/
fun cardProcessed(scannedCard: ScannedCard)
}
internal const val INTENT_PARAM_REQUEST = "request"
internal const val INTENT_PARAM_RESULT = "result"
@Deprecated("Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
open class CardScanActivity :
CardScanBaseActivity(),
AggregateResultListener,
CompletionLoopListener,
AnalyzerLoopErrorListener {
/**
* And overlay to darken the screen during result processing.
*/
protected open val processingOverlayView by lazy { View(this) }
/**
* The spinner indicating that results are processing.
*/
protected open val processingSpinnerView by lazy { ProgressBar(this) }
/**
* The text indicating that results are processing
*/
protected open val processingTextView by lazy { TextView(this) }
/**
* The image view for debugging the completion loop
*/
protected open val debugCompletionImageView by lazy { ImageView(this) }
override fun addUiComponents() {
super.addUiComponents()
appendUiComponents(processingOverlayView, processingSpinnerView, processingTextView, debugCompletionImageView)
}
override fun setupUiComponents() {
super.setupUiComponents()
setupProcessingOverlayViewUi()
setupProcessingTextViewUi()
setupDebugCompletionViewUi()
}
protected open fun setupProcessingOverlayViewUi() {
processingOverlayView.setBackgroundColor(getColorByRes(R.color.bouncerProcessingBackground))
}
protected open fun setupProcessingTextViewUi() {
processingTextView.text = getString(R.string.bouncer_processing_card)
processingTextView.setTextSizeByRes(R.dimen.bouncerProcessingTextSize)
processingTextView.setTextColor(getColorByRes(R.color.bouncerProcessingText))
processingTextView.gravity = Gravity.CENTER
}
protected open fun setupDebugCompletionViewUi() {
debugCompletionImageView.contentDescription = getString(R.string.bouncer_debug_description)
debugCompletionImageView.setVisible(Config.isDebug)
}
override fun setupUiConstraints() {
super.setupUiConstraints()
setupProcessingOverlayViewConstraints()
setupProcessingSpinnerViewConstraints()
setupProcessingTextViewConstraints()
setupDebugCompletionViewConstraints()
}
protected open fun setupProcessingOverlayViewConstraints() {
processingOverlayView.layoutParams = ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.MATCH_PARENT, // width
ConstraintLayout.LayoutParams.MATCH_PARENT, // height
)
processingOverlayView.constrainToParent()
}
protected open fun setupProcessingSpinnerViewConstraints() {
processingSpinnerView.layoutParams = ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.WRAP_CONTENT, // width
ConstraintLayout.LayoutParams.WRAP_CONTENT, // height
)
processingSpinnerView.constrainToParent()
}
protected open fun setupProcessingTextViewConstraints() {
processingTextView.layoutParams = ConstraintLayout.LayoutParams(
0, // width
ConstraintLayout.LayoutParams.WRAP_CONTENT, // height
)
processingTextView.addConstraints {
connect(it.id, ConstraintSet.TOP, processingSpinnerView.id, ConstraintSet.BOTTOM)
connect(it.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
connect(it.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
}
}
protected open fun setupDebugCompletionViewConstraints() {
debugCompletionImageView.layoutParams = ConstraintLayout.LayoutParams(
resources.getDimensionPixelSize(R.dimen.bouncerDebugWindowWidth), // width,
resources.getDimensionPixelSize(R.dimen.bouncerDebugWindowWidth), // height
)
debugCompletionImageView.addConstraints {
connect(it.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
connect(it.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
}
}
override fun displayState(newState: ScanState, previousState: ScanState?) {
super.displayState(newState, previousState)
when (newState) {
is ScanState.NotFound, ScanState.FoundShort, ScanState.FoundLong, ScanState.Wrong -> {
processingOverlayView.hide()
processingSpinnerView.hide()
processingTextView.hide()
}
is ScanState.Correct -> {
processingOverlayView.show()
processingSpinnerView.show()
processingTextView.show()
}
}
}
override val scanFlow: CardScanFlow by lazy {
CardScanFlow(enableNameExtraction, enableExpiryExtraction, this, this)
}
private val params: CardScanSheetParams by lazy {
intent.getParcelableExtra(INTENT_PARAM_REQUEST)
?: CardScanSheetParams(
apiKey = "",
enableEnterManually = true,
enableNameExtraction = false,
enableExpiryExtraction = false,
)
}
override val enableEnterCardManually: Boolean by lazy { params.enableEnterManually }
override val enableNameExtraction: Boolean by lazy { params.enableNameExtraction }
override val enableExpiryExtraction: Boolean by lazy { params.enableExpiryExtraction }
private var pan: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Config.apiKey = params.apiKey
}
override val resultListener = object : CardProcessedResultListener {
override fun cardProcessed(scannedCard: ScannedCard) {
val intent = Intent()
.putExtra(
INTENT_PARAM_RESULT,
CardScanSheetResult.Completed(scannedCard)
)
setResult(Activity.RESULT_OK, intent)
closeScanner()
}
override fun cardScanned(
pan: String?,
frames: Collection,
isFastDevice: Boolean
) {
this@CardScanActivity.pan = pan
launch(Dispatchers.Default) {
scanFlow.launchCompletionLoop(
context = this@CardScanActivity,
completionResultListener = this@CardScanActivity,
savedFrames = frames,
isFastDevice = isFastDevice,
coroutineScope = this@CardScanActivity,
)
}
}
override fun userCanceled(reason: CancellationReason) {
val intent = Intent()
.putExtra(
INTENT_PARAM_RESULT,
CardScanSheetResult.Canceled(reason),
)
setResult(Activity.RESULT_CANCELED, intent)
}
override fun failed(cause: Throwable?) {
val intent = Intent()
.putExtra(
INTENT_PARAM_RESULT,
CardScanSheetResult.Failed(cause ?: UnknownScanException()),
)
setResult(Activity.RESULT_CANCELED, intent)
}
}
override fun onCompletionLoopDone(result: CompletionLoopResult) = launch(Dispatchers.Main) {
scanStat.trackResult("card_scanned")
// Only show the expiry dates that are not expired
val (expiryMonth, expiryYear) = if (isValidExpiry(null, result.expiryMonth ?: "", result.expiryYear ?: "")) {
(result.expiryMonth to result.expiryYear)
} else {
(null to null)
}
resultListener.cardProcessed(
scannedCard = ScannedCard(
pan = pan,
expiryDay = null,
expiryMonth = expiryMonth,
expiryYear = expiryYear,
networkName = getCardIssuer(pan).displayName,
cvc = null,
cardholderName = result.name,
errorString = result.errorString,
)
)
}.let { }
override fun onCompletionLoopFrameProcessed(
result: CompletionLoopAnalyzer.Prediction,
frame: SavedFrame,
) = launch(Dispatchers.Main) {
if (Config.isDebug) {
val bitmap = withContext(Dispatchers.Default) {
cropCameraPreviewToSquare(frame.frame.cameraPreviewImage.image.image, frame.frame.cameraPreviewImage.previewImageBounds, frame.frame.cardFinder)
}
debugCompletionImageView.setImageBitmap(bitmap)
}
}.let { }
}
================================================
FILE: cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/CardScanBaseActivity.kt
================================================
package com.getbouncer.cardscan.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Size
import android.view.Gravity
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import com.getbouncer.cardscan.ui.result.MainLoopAggregator
import com.getbouncer.cardscan.ui.result.MainLoopState
import com.getbouncer.scan.framework.AggregateResultListener
import com.getbouncer.scan.framework.AnalyzerLoopErrorListener
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.payment.card.formatPan
import com.getbouncer.scan.payment.cropCameraPreviewToSquare
import com.getbouncer.scan.payment.cropCameraPreviewToViewFinder
import com.getbouncer.scan.payment.ml.ssd.DetectionBox
import com.getbouncer.scan.ui.CancellationReason
import com.getbouncer.scan.ui.DebugDetectionBox
import com.getbouncer.scan.ui.ScanResultListener
import com.getbouncer.scan.ui.SimpleScanActivity
import com.getbouncer.scan.ui.util.getColorByRes
import com.getbouncer.scan.ui.util.setTextSizeByRes
import com.getbouncer.scan.ui.util.setVisible
import com.getbouncer.scan.ui.util.show
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean
@Deprecated("Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
interface CardScanResultListener : ScanResultListener {
/**
* A payment card was successfully scanned.
*/
fun cardScanned(
pan: String?,
frames: Collection,
isFastDevice: Boolean,
)
}
private val MINIMUM_RESOLUTION = Size(1067, 600) // minimum size of screen detect
private fun DetectionBox.forDebug() = DebugDetectionBox(rect, confidence, label.toString())
@Deprecated("Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
abstract class CardScanBaseActivity :
SimpleScanActivity(),
AggregateResultListener,
AnalyzerLoopErrorListener {
/**
* The text view that lets a user manually enter a card.
*/
protected open val enterCardManuallyTextView: TextView by lazy { TextView(this) }
protected abstract val enableEnterCardManually: Boolean
protected abstract val enableNameExtraction: Boolean
protected abstract val enableExpiryExtraction: Boolean
/**
* The listener which handles results from the scan.
*/
abstract override val resultListener: CardScanResultListener
private var mainLoopIsProducingResults = AtomicBoolean(false)
private val hasPreviousValidResult = AtomicBoolean(false)
abstract override val scanFlow: CardScanFlow
override val minimumAnalysisResolution: Size = MINIMUM_RESOLUTION
/**
* During on create
*/
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterCardManuallyTextView.setOnClickListener { enterCardManually() }
}
override fun addUiComponents() {
super.addUiComponents()
appendUiComponents(enterCardManuallyTextView)
}
override fun setupUiComponents() {
super.setupUiComponents()
enterCardManuallyTextView.text = getString(R.string.bouncer_enter_card_manually)
enterCardManuallyTextView.setTextSizeByRes(R.dimen.bouncerEnterCardManuallyTextSize)
enterCardManuallyTextView.gravity = Gravity.CENTER
enterCardManuallyTextView.setVisible(enableEnterCardManually)
if (isBackgroundDark()) {
enterCardManuallyTextView.setTextColor(getColorByRes(R.color.bouncerEnterCardManuallyColorDark))
} else {
enterCardManuallyTextView.setTextColor(getColorByRes(R.color.bouncerEnterCardManuallyColorLight))
}
}
override fun setupUiConstraints() {
super.setupUiConstraints()
enterCardManuallyTextView.layoutParams = ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.WRAP_CONTENT, // width
ConstraintLayout.LayoutParams.WRAP_CONTENT, // height
).apply {
marginStart = resources.getDimensionPixelSize(R.dimen.bouncerEnterCardManuallyMargin)
marginEnd = resources.getDimensionPixelSize(R.dimen.bouncerEnterCardManuallyMargin)
bottomMargin = resources.getDimensionPixelSize(R.dimen.bouncerEnterCardManuallyMargin)
topMargin = resources.getDimensionPixelSize(R.dimen.bouncerEnterCardManuallyMargin)
}
enterCardManuallyTextView.addConstraints {
connect(it.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
connect(it.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
connect(it.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
}
}
/**
* Cancel scanning to enter a card manually
*/
protected open fun enterCardManually() {
runBlocking { scanStat.trackResult("enter_card_manually") }
resultListener.userCanceled(CancellationReason.UserCannotScan)
closeScanner()
}
/**
* A final result was received from the aggregator.
*/
override suspend fun onResult(result: MainLoopAggregator.FinalResult) = launch(Dispatchers.Main) {
changeScanState(ScanState.Correct)
cameraAdapter.unbindFromLifecycle(this@CardScanBaseActivity)
resultListener.cardScanned(
pan = result.pan,
frames = scanFlow.selectCompletionLoopFrames(result.averageFrameRate, result.savedFrames),
isFastDevice = result.averageFrameRate > Config.slowDeviceFrameRate,
)
}.let { }
/**
* An interim result was received from the result aggregator.
*/
override suspend fun onInterimResult(result: MainLoopAggregator.InterimResult) = launch(Dispatchers.Main) {
if (!mainLoopIsProducingResults.getAndSet(true)) {
scanStat.trackResult("first_image_processed")
}
if (result.state is MainLoopState.PanFound && !hasPreviousValidResult.getAndSet(true)) {
scanStat.trackResult("ocr_pan_observed")
}
if (Config.displayScanResult) {
if (Config.isDebug && result.analyzerResult.ocr?.pan?.isNotEmpty() == true) {
cardNumberTextView.text = formatPan(result.analyzerResult.ocr.pan)
cardNumberTextView.show()
} else {
val mostLikelyPan = when (val state = result.state) {
is MainLoopState.Initial -> null
is MainLoopState.PanFound -> state.getMostLikelyPan()
is MainLoopState.PanSatisfied -> state.pan
is MainLoopState.CardSatisfied -> state.getMostLikelyPan()
is MainLoopState.Finished -> state.pan
}
if (mostLikelyPan?.isNotEmpty() == true) {
cardNumberTextView.text = formatPan(mostLikelyPan)
cardNumberTextView.show()
}
}
}
when (result.state) {
is MainLoopState.Initial -> if (scanState !is ScanState.FoundLong) changeScanState(ScanState.NotFound)
is MainLoopState.PanFound -> changeScanState(ScanState.FoundLong)
is MainLoopState.PanSatisfied -> changeScanState(ScanState.FoundLong)
is MainLoopState.CardSatisfied -> changeScanState(ScanState.FoundLong)
is MainLoopState.Finished -> changeScanState(ScanState.Correct)
}
if (Config.isDebug) {
result.analyzerResult.ocr?.detectedBoxes?.let { detectionBoxes ->
val bitmap = withContext(Dispatchers.Default) {
cropCameraPreviewToViewFinder(
result.frame.cameraPreviewImage.image.image,
result.frame.cameraPreviewImage.previewImageBounds,
result.frame.cardFinder
)
}
debugImageView.setImageBitmap(bitmap)
debugOverlayView.setBoxes(detectionBoxes.map { it.forDebug() })
} ?: run {
val bitmap = withContext(Dispatchers.Default) {
cropCameraPreviewToSquare(
result.frame.cameraPreviewImage.image.image,
result.frame.cameraPreviewImage.previewImageBounds,
result.frame.cardFinder
)
}
debugImageView.setImageBitmap(bitmap)
debugOverlayView.clearBoxes()
}
}
}.let { }
override suspend fun onReset() = launch(Dispatchers.Main) { changeScanState(ScanState.NotFound) }.let { }
override fun onAnalyzerFailure(t: Throwable): Boolean {
analyzerFailureCancelScan(t)
return true
}
override fun onResultFailure(t: Throwable): Boolean {
analyzerFailureCancelScan(t)
return true
}
}
================================================
FILE: cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/CardScanFlow.kt
================================================
package com.getbouncer.cardscan.ui
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Rect
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.lifecycle.LifecycleOwner
import com.getbouncer.cardscan.ui.analyzer.CompletionLoopAnalyzer
import com.getbouncer.cardscan.ui.analyzer.MainLoopAnalyzer
import com.getbouncer.cardscan.ui.result.CompletionLoopAggregator
import com.getbouncer.cardscan.ui.result.CompletionLoopListener
import com.getbouncer.cardscan.ui.result.CompletionLoopResult
import com.getbouncer.cardscan.ui.result.MainLoopAggregator
import com.getbouncer.cardscan.ui.result.MainLoopState
import com.getbouncer.scan.camera.CameraAdapter
import com.getbouncer.scan.camera.CameraPreviewImage
import com.getbouncer.scan.framework.AggregateResultListener
import com.getbouncer.scan.framework.AnalyzerLoopErrorListener
import com.getbouncer.scan.framework.AnalyzerPool
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.framework.FetchedData
import com.getbouncer.scan.framework.FiniteAnalyzerLoop
import com.getbouncer.scan.framework.ProcessBoundAnalyzerLoop
import com.getbouncer.scan.framework.time.Duration
import com.getbouncer.scan.framework.time.Rate
import com.getbouncer.scan.payment.FrameDetails
import com.getbouncer.scan.payment.TextDetectModelManager
import com.getbouncer.scan.payment.analyzer.NameAndExpiryAnalyzer
import com.getbouncer.scan.payment.ml.AlphabetDetect
import com.getbouncer.scan.payment.ml.AlphabetDetectModelManager
import com.getbouncer.scan.payment.ml.CardDetect
import com.getbouncer.scan.payment.ml.CardDetectModelManager
import com.getbouncer.scan.payment.ml.ExpiryDetect
import com.getbouncer.scan.payment.ml.ExpiryDetectModelManager
import com.getbouncer.scan.payment.ml.SSDOcr
import com.getbouncer.scan.payment.ml.SSDOcrModelManager
import com.getbouncer.scan.payment.ml.TextDetect
import com.getbouncer.scan.ui.ScanFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@Deprecated("Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class SavedFrame(
val pan: String?,
val frame: MainLoopAnalyzer.Input,
val details: FrameDetails,
)
@Deprecated("Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class SavedFrameType(
val hasCard: Boolean,
val hasPan: Boolean,
)
/**
* This class contains the logic required for analyzing a credit card for scanning.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Deprecated("Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
open class CardScanFlow(
private val enableNameExtraction: Boolean,
private val enableExpiryExtraction: Boolean,
private val scanResultListener: AggregateResultListener,
private val scanErrorListener: AnalyzerLoopErrorListener,
) : ScanFlow {
companion object {
private const val MAX_COMPLETION_LOOP_FRAMES_FAST_DEVICE = 8
private const val MAX_COMPLETION_LOOP_FRAMES_SLOW_DEVICE = 5
/**
* Warm up the analyzers for card scanner. This method is optional, but will increase the speed at which the
* scan occurs.
*
* @param context: A context to use for warming up the analyzers.
*/
@JvmStatic
@Deprecated("Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
suspend fun prepareScan(
context: Context,
apiKey: String,
initializeNameAndExpiryExtraction: Boolean,
forImmediateUse: Boolean,
) = withContext(Dispatchers.IO) {
Config.apiKey = apiKey
val deferredFetchers = mutableListOf>()
deferredFetchers.add(async { SSDOcrModelManager.fetchModel(context, forImmediateUse) })
deferredFetchers.add(async { CardDetectModelManager.fetchModel(context, forImmediateUse) })
if (initializeNameAndExpiryExtraction) {
deferredFetchers.add(async { TextDetectModelManager.fetchModel(context, forImmediateUse) })
deferredFetchers.add(async { AlphabetDetectModelManager.fetchModel(context, forImmediateUse) })
deferredFetchers.add(async { ExpiryDetectModelManager.fetchModel(context, forImmediateUse) })
}
deferredFetchers.fold(true) { acc, deferred -> acc && deferred.await().successfullyFetched }
}
/**
* Determine if the scan is supported
*/
@JvmStatic
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan")
)
fun isSupported(context: Context) = CameraAdapter.isCameraSupported(context)
/**
* Determine if the scan models are available (have been warmed up)
*/
@JvmStatic
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan")
)
fun isScanReady() = runBlocking { SSDOcrModelManager.isReady() && CardDetectModelManager.isReady() }
}
/**
* If this is true, do not start the flow.
*/
private var canceled = false
private var mainLoopAnalyzerPool: AnalyzerPool? = null
private var mainLoopAggregator: MainLoopAggregator? = null
private var mainLoop: ProcessBoundAnalyzerLoop? = null
private var mainLoopJob: Job? = null
private var completionLoopAnalyzerPool: AnalyzerPool? = null
private var completionLoop: FiniteAnalyzerLoop? = null
private var completionLoopJob: Job? = null
/**
* Start the image processing flow for scanning a card.
*
* @param context: The context used to download analyzers if needed
* @param imageStream: The flow of images to process
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan")
)
override fun startFlow(
context: Context,
imageStream: Flow>,
viewFinder: Rect,
lifecycleOwner: LifecycleOwner,
coroutineScope: CoroutineScope
) = coroutineScope.launch(Dispatchers.Main) {
val listener =
object : AggregateResultListener {
override suspend fun onResult(result: MainLoopAggregator.FinalResult) {
mainLoop?.unsubscribe()
mainLoop = null
mainLoopJob?.apply { if (isActive) { cancel() } }
mainLoopJob = null
mainLoopAggregator = null
mainLoopAnalyzerPool?.closeAllAnalyzers()
mainLoopAnalyzerPool = null
scanResultListener.onResult(result)
}
override suspend fun onInterimResult(result: MainLoopAggregator.InterimResult) {
scanResultListener.onInterimResult(result)
}
override suspend fun onReset() {
scanResultListener.onReset()
}
}
if (canceled) {
return@launch
}
mainLoopAggregator = MainLoopAggregator(listener).also { aggregator ->
// make this result aggregator pause and reset when the lifecycle pauses.
aggregator.bindToLifecycle(lifecycleOwner)
val analyzerPool = AnalyzerPool.of(
MainLoopAnalyzer.Factory(
SSDOcr.Factory(context, SSDOcrModelManager.fetchModel(context, forImmediateUse = true, isOptional = false)),
CardDetect.Factory(context, CardDetectModelManager.fetchModel(context, forImmediateUse = true, isOptional = false)),
)
)
mainLoopAnalyzerPool = analyzerPool
mainLoop = ProcessBoundAnalyzerLoop(
analyzerPool = analyzerPool,
resultHandler = aggregator,
analyzerLoopErrorListener = scanErrorListener,
).apply {
mainLoopJob = subscribeTo(
imageStream.map {
MainLoopAnalyzer.Input(
cameraPreviewImage = it,
cardFinder = viewFinder,
)
},
coroutineScope,
)
}
}
}.let { }
/**
* In the event that the scan cannot complete, halt the flow to halt analyzers and free up CPU and memory.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan")
)
override fun cancelFlow() {
canceled = true
mainLoopAggregator?.run { cancel() }
mainLoopAggregator = null
mainLoop?.unsubscribe()
mainLoop = null
mainLoopAnalyzerPool?.closeAllAnalyzers()
mainLoopAnalyzerPool = null
mainLoopJob?.apply { if (isActive) { cancel() } }
mainLoopJob = null
completionLoop?.cancel()
completionLoop = null
completionLoopAnalyzerPool?.closeAllAnalyzers()
completionLoopAnalyzerPool = null
completionLoopJob?.apply { if (isActive) { cancel() } }
completionLoopJob = null
}
open fun launchCompletionLoop(
context: Context,
completionResultListener: CompletionLoopListener,
savedFrames: Collection,
isFastDevice: Boolean,
coroutineScope: CoroutineScope,
) = coroutineScope.launch {
if (canceled) {
return@launch
}
val analyzerPool = AnalyzerPool.of(
CompletionLoopAnalyzer.Factory(
nameAndExpiryFactory = NameAndExpiryAnalyzer.Factory(
textDetectFactory = TextDetect.Factory(
context,
TextDetectModelManager.fetchModel(context, forImmediateUse = true, isOptional = true)
),
alphabetDetectFactory = AlphabetDetect.Factory(
context,
AlphabetDetectModelManager.fetchModel(context, forImmediateUse = true, isOptional = true)
),
expiryDetectFactory = ExpiryDetect.Factory(
context,
ExpiryDetectModelManager.fetchModel(context, forImmediateUse = true, isOptional = true)
),
runNameExtraction = enableNameExtraction && isFastDevice,
runExpiryExtraction = enableExpiryExtraction,
)
)
)
completionLoopAnalyzerPool = analyzerPool
completionLoop = FiniteAnalyzerLoop(
analyzerPool = analyzerPool,
resultHandler = CompletionLoopAggregator(object : CompletionLoopListener {
override fun onCompletionLoopDone(result: CompletionLoopResult) {
completionLoop = null
completionLoopAnalyzerPool?.closeAllAnalyzers()
completionLoopAnalyzerPool = null
completionLoopJob?.apply { if (isActive) { cancel() } }
completionLoopJob = null
completionResultListener.onCompletionLoopDone(result)
}
override fun onCompletionLoopFrameProcessed(
result: CompletionLoopAnalyzer.Prediction,
frame: SavedFrame
) = completionResultListener.onCompletionLoopFrameProcessed(result, frame)
}),
analyzerLoopErrorListener = object : AnalyzerLoopErrorListener {
override fun onAnalyzerFailure(t: Throwable): Boolean {
Log.e(Config.logTag, "Completion loop analyzer failure", t)
completionResultListener.onCompletionLoopDone(CompletionLoopResult())
return true // terminate the loop on any analyzer failures
}
override fun onResultFailure(t: Throwable): Boolean {
Log.e(Config.logTag, "Completion loop result failures", t)
completionResultListener.onCompletionLoopDone(CompletionLoopResult())
return true // terminate the loop on any result failures
}
}
).apply {
completionLoopJob = process(savedFrames, coroutineScope)
}
}.let { }
/**
* Select which frames to use in the completion loop.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan")
)
open fun selectCompletionLoopFrames(
frameRate: Rate,
frames: Map>,
): Collection {
fun getFrames(frameType: SavedFrameType) = frames[frameType] ?: emptyList()
val cardAndPan = getFrames(SavedFrameType(hasCard = true, hasPan = true))
val card = getFrames(SavedFrameType(hasCard = true, hasPan = false))
val pan = getFrames(SavedFrameType(hasCard = false, hasPan = true))
val maxCompletionLoopFrames =
if (frameRate.duration <= Duration.ZERO || frameRate > Config.slowDeviceFrameRate) {
MAX_COMPLETION_LOOP_FRAMES_FAST_DEVICE
} else {
MAX_COMPLETION_LOOP_FRAMES_SLOW_DEVICE
}
return (cardAndPan + card + pan).take(maxCompletionLoopFrames)
}
}
================================================
FILE: cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/CardScanSheet.kt
================================================
package com.getbouncer.cardscan.ui
import android.content.Context
import android.content.Intent
import android.os.Parcelable
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.contract.ActivityResultContract
import androidx.fragment.app.Fragment
import com.getbouncer.cardscan.ui.exception.UnknownScanException
import com.getbouncer.scan.ui.CancellationReason
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@Parcelize
internal data class CardScanSheetParams(
val apiKey: String,
val enableEnterManually: Boolean,
val enableNameExtraction: Boolean,
val enableExpiryExtraction: Boolean,
) : Parcelable
@Deprecated("Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
sealed interface CardScanSheetResult : Parcelable {
@Parcelize
data class Completed(
val scannedCard: ScannedCard,
) : CardScanSheetResult
@Parcelize
data class Canceled(
val reason: CancellationReason,
) : CardScanSheetResult
@Parcelize
data class Failed(val error: Throwable) : CardScanSheetResult
}
/**
* @Deprecated in favor of Stripe CardScan. This code was migrated to Stripe CardScan and is no
* longer maintained in this repository. For the up-to-date version of this SDK, please visit
* https://github.com/stripe/stripe-android/tree/master/stripecardscan
*/
@Deprecated("Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class CardScanSheet private constructor(private val apiKey: String) {
private lateinit var launcher: ActivityResultLauncher
/**
* Callback to notify when scanning finishes and a result is available.
*/
fun interface CardScanResultCallback {
fun onCardScanSheetResult(cardScanSheetResult: CardScanSheetResult)
}
companion object {
/**
* Create a [CardScanSheet] instance with [ComponentActivity].
*
* This API registers an [ActivityResultLauncher] into the
* [ComponentActivity], it must be called before the [ComponentActivity]
* is created (in the onCreate method).
*/
@JvmStatic
@JvmOverloads
@Deprecated("Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun create(
from: ComponentActivity,
apiKey: String,
cardScanResultCallback: CardScanResultCallback,
registry: ActivityResultRegistry = from.activityResultRegistry,
) = CardScanSheet(apiKey).apply {
launcher = from.registerForActivityResult(
activityResultContract,
registry,
cardScanResultCallback::onCardScanSheetResult,
)
}
@JvmStatic
@JvmOverloads
@Deprecated("Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun create(
from: Fragment,
apiKey: String,
cardScanResultCallback: CardScanResultCallback,
registry: ActivityResultRegistry? = null,
) = CardScanSheet(apiKey).apply {
launcher = if (registry != null) {
from.registerForActivityResult(
activityResultContract,
registry,
cardScanResultCallback::onCardScanSheetResult,
)
} else {
from.registerForActivityResult(
activityResultContract,
cardScanResultCallback::onCardScanSheetResult,
)
}
}
private val activityResultContract =
object : ActivityResultContract() {
override fun createIntent(
context: Context,
input: CardScanSheetParams,
) = this@Companion.createIntent(context, input)
override fun parseResult(
resultCode: Int,
intent: Intent?,
) = this@Companion.parseResult(intent)
}
private fun createIntent(context: Context, input: CardScanSheetParams) =
Intent(context, CardScanActivity::class.java)
.putExtra(INTENT_PARAM_REQUEST, input)
private fun parseResult(intent: Intent?): CardScanSheetResult =
intent?.getParcelableExtra(INTENT_PARAM_RESULT)
?: CardScanSheetResult.Failed(
UnknownScanException("No data in the result intent")
)
/**
* Determine if the scan is supported
*/
@JvmStatic
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan")
)
fun isSupported(context: Context) = CardScanFlow.isSupported(context)
/**
* Determine if the scan models are available (have been warmed up)
*/
@JvmStatic
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan")
)
fun isScanReady() = CardScanFlow.isScanReady()
/**
* Warm up the analyzers and call [onPrepared] once the scan is ready.
*
* @param context: A context to use for warming up the analyzers.
* @param apiKey: the API key used to warm up the ML models
* @param initializeNameAndExpiryExtraction: if true, include name and expiry extraction
* @param onPrepared: called once the scan is ready
*/
@JvmStatic
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan")
)
fun prepareScan(
context: Context,
apiKey: String,
initializeNameAndExpiryExtraction: Boolean,
onPrepared: () -> Unit,
) = GlobalScope.launch {
CardScanFlow.prepareScan(context, apiKey, initializeNameAndExpiryExtraction, false)
}.invokeOnCompletion { onPrepared() }
}
/**
* Present the CardScan flow.
* Results will be returned in the callback function.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan")
)
fun present(
enableEnterManually: Boolean,
enableNameExtraction: Boolean,
enableExpiryExtraction: Boolean,
) {
launcher.launch(
CardScanSheetParams(
apiKey = apiKey,
enableEnterManually = enableEnterManually,
enableNameExtraction = enableNameExtraction,
enableExpiryExtraction = enableExpiryExtraction,
)
)
}
}
================================================
FILE: cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/analyzer/CompletionLoopAnalyzer.kt
================================================
package com.getbouncer.cardscan.ui.analyzer
import com.getbouncer.cardscan.ui.SavedFrame
import com.getbouncer.scan.framework.Analyzer
import com.getbouncer.scan.framework.AnalyzerFactory
import com.getbouncer.scan.payment.analyzer.NameAndExpiryAnalyzer
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class CompletionLoopAnalyzer private constructor(
private val nameAndExpiryAnalyzer: NameAndExpiryAnalyzer?,
) : Analyzer {
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class Prediction(
val nameAndExpiryResult: NameAndExpiryAnalyzer.Prediction?,
val isNameExtractionAvailable: Boolean,
val isExpiryExtractionAvailable: Boolean,
val enableNameExtraction: Boolean,
val enableExpiryExtraction: Boolean,
)
override suspend fun analyze(
data: SavedFrame,
state: Unit,
) = Prediction(
nameAndExpiryResult = nameAndExpiryAnalyzer?.analyze(
NameAndExpiryAnalyzer.Input(data.frame.cameraPreviewImage.image, data.frame.cameraPreviewImage.previewImageBounds, data.frame.cardFinder),
state,
),
isNameExtractionAvailable = nameAndExpiryAnalyzer?.isNameDetectorAvailable() ?: false,
isExpiryExtractionAvailable = nameAndExpiryAnalyzer?.isExpiryDetectorAvailable() ?: false,
enableNameExtraction = nameAndExpiryAnalyzer?.runNameExtraction ?: false,
enableExpiryExtraction = nameAndExpiryAnalyzer?.runExpiryExtraction ?: false,
)
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class Factory(
private val nameAndExpiryFactory: AnalyzerFactory,
) : AnalyzerFactory {
override suspend fun newInstance() = CompletionLoopAnalyzer(
nameAndExpiryAnalyzer = nameAndExpiryFactory.newInstance(),
)
}
}
================================================
FILE: cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/analyzer/MainLoopAnalyzer.kt
================================================
package com.getbouncer.cardscan.ui.analyzer
import android.graphics.Bitmap
import android.graphics.Rect
import com.getbouncer.cardscan.ui.result.MainLoopState
import com.getbouncer.scan.camera.CameraPreviewImage
import com.getbouncer.scan.framework.Analyzer
import com.getbouncer.scan.framework.AnalyzerFactory
import com.getbouncer.scan.payment.ml.CardDetect
import com.getbouncer.scan.payment.ml.SSDOcr
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class MainLoopAnalyzer(
private val ssdOcr: Analyzer?,
private val cardDetect: Analyzer?,
) : Analyzer {
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class Input(
val cameraPreviewImage: CameraPreviewImage,
val cardFinder: Rect,
)
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class Prediction(
val ocr: SSDOcr.Prediction?,
val card: CardDetect.Prediction?,
) {
val isCardVisible = card?.side?.let { it == CardDetect.Prediction.Side.NO_PAN || it == CardDetect.Prediction.Side.PAN }
}
override suspend fun analyze(data: Input, state: MainLoopState): Prediction {
val cardResult = if (state.runCardDetect) cardDetect?.analyze(CardDetect.cameraPreviewToInput(data.cameraPreviewImage.image, data.cameraPreviewImage.previewImageBounds, data.cardFinder), Unit) else null
val ocrResult = if (state.runOcr) ssdOcr?.analyze(SSDOcr.cameraPreviewToInput(data.cameraPreviewImage.image, data.cameraPreviewImage.previewImageBounds, data.cardFinder), Unit) else null
return Prediction(
ocr = ocrResult,
card = cardResult,
)
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class Factory(
private val ssdOcrFactory: AnalyzerFactory>,
private val cardDetectFactory: AnalyzerFactory>,
) : AnalyzerFactory {
override suspend fun newInstance(): MainLoopAnalyzer = MainLoopAnalyzer(
ssdOcr = ssdOcrFactory.newInstance(),
cardDetect = cardDetectFactory.newInstance(),
)
}
}
================================================
FILE: cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/exception/StripeNetworkException.kt
================================================
package com.getbouncer.cardscan.ui.exception
import java.lang.Exception
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class StripeNetworkException(message: String) : Exception(message)
================================================
FILE: cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/exception/UnknownScanException.kt
================================================
package com.getbouncer.cardscan.ui.exception
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class UnknownScanException(message: String? = null) : Exception(message)
================================================
FILE: cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/result/CompletionLoopAggregator.kt
================================================
package com.getbouncer.cardscan.ui.result
import com.getbouncer.cardscan.ui.SavedFrame
import com.getbouncer.cardscan.ui.analyzer.CompletionLoopAnalyzer
import com.getbouncer.scan.framework.TerminatingResultHandler
import com.getbouncer.scan.framework.time.Duration
import com.getbouncer.scan.framework.util.FrameRateTracker
import com.getbouncer.scan.framework.util.ItemCounter
import com.getbouncer.scan.framework.util.ItemTotalCounter
import com.getbouncer.scan.payment.ml.ExpiryDetect
private const val MINIMUM_NAME_AGREEMENT = 2
private const val MINIMUM_EXPIRY_AGREEMENT = 2
private const val INSUFFICIENT_PERMISSIONS_PREFIX = "Insufficient API key permissions - "
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
interface CompletionLoopListener {
fun onCompletionLoopDone(result: CompletionLoopResult)
fun onCompletionLoopFrameProcessed(
result: CompletionLoopAnalyzer.Prediction,
frame: SavedFrame,
)
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class CompletionLoopResult(
val name: String? = null,
val expiryMonth: String? = null,
val expiryYear: String? = null,
val errorString: String? = null,
)
/**
* Collect the results from executing the completion loop across multiple saved images. Send the
* collected results to the [listener].
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class CompletionLoopAggregator(
private val listener: CompletionLoopListener,
) : TerminatingResultHandler(Unit) {
private val frameRateTracker by lazy {
FrameRateTracker("cardscan_completion_loop_aggregator", notifyInterval = Duration.ZERO)
}
private val nameCounter: ItemCounter = ItemTotalCounter()
private val expiryCounter: ItemCounter = ItemTotalCounter()
private val errors = mutableSetOf()
override suspend fun onResult(
result: CompletionLoopAnalyzer.Prediction,
data: SavedFrame,
) {
result.nameAndExpiryResult?.let { prediction ->
prediction.name?.let { nameCounter.countItem(it) }
prediction.expiry?.let { expiryCounter.countItem(it) }
}
if (!result.isNameExtractionAvailable && result.enableNameExtraction) {
errors.add("name")
}
if (!result.isExpiryExtractionAvailable && result.enableExpiryExtraction) {
errors.add("expiry")
}
frameRateTracker.trackFrameProcessed()
listener.onCompletionLoopFrameProcessed(result, data)
}
override suspend fun onAllDataProcessed() {
val expiry = expiryCounter.getHighestCountItem(MINIMUM_EXPIRY_AGREEMENT)?.second
listener.onCompletionLoopDone(
CompletionLoopResult(
name = nameCounter.getHighestCountItem(MINIMUM_NAME_AGREEMENT)?.second,
expiryMonth = expiry?.month,
expiryYear = expiry?.year,
errorString = if (errors.isNotEmpty()) {
INSUFFICIENT_PERMISSIONS_PREFIX + errors.joinToString(",", prefix = "[", postfix = "]")
} else null,
)
)
}
override suspend fun onTerminatedEarly() {
val expiry = expiryCounter.getHighestCountItem(MINIMUM_EXPIRY_AGREEMENT)?.second
listener.onCompletionLoopDone(
CompletionLoopResult(
name = nameCounter.getHighestCountItem(MINIMUM_NAME_AGREEMENT)?.second,
expiryMonth = expiry?.month,
expiryYear = expiry?.year,
errorString = if (errors.isNotEmpty()) {
INSUFFICIENT_PERMISSIONS_PREFIX + errors.joinToString(",", prefix = "[", postfix = "]")
} else null,
)
)
}
}
================================================
FILE: cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/result/MainLoopAggregator.kt
================================================
package com.getbouncer.cardscan.ui.result
import android.util.Log
import androidx.annotation.Keep
import com.getbouncer.cardscan.ui.SavedFrame
import com.getbouncer.cardscan.ui.SavedFrameType
import com.getbouncer.cardscan.ui.analyzer.MainLoopAnalyzer
import com.getbouncer.scan.framework.AggregateResultListener
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.framework.ResultAggregator
import com.getbouncer.scan.framework.time.Rate
import com.getbouncer.scan.framework.util.FrameSaver
import com.getbouncer.scan.payment.FrameDetails
import com.getbouncer.scan.payment.card.isValidPan
import kotlinx.coroutines.runBlocking
private const val MAX_SAVED_FRAMES_PER_TYPE = 6
/**
* Aggregate results from the main loop. Each frame will trigger an [InterimResult] to the [listener]. Once the
* [MainLoopState.Finished] state is reached, a [FinalResult] will be sent to the [listener].
*
* This aggregator is a state machine. The full list of possible states are subclasses of [MainLoopState]. This was
* written referencing this article: https://thoughtbot.com/blog/finite-state-machines-android-kotlin-good-times
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class MainLoopAggregator(
listener: AggregateResultListener,
) : ResultAggregator(
listener = listener,
initialState = MainLoopState.Initial()
) {
@Keep
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class FinalResult(
val pan: String,
val savedFrames: Map>,
val averageFrameRate: Rate,
)
@Keep
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class InterimResult(
val analyzerResult: MainLoopAnalyzer.Prediction,
val frame: MainLoopAnalyzer.Input,
val state: MainLoopState,
)
private val frameSaver = object : FrameSaver() {
override fun getMaxSavedFrames(savedFrameIdentifier: SavedFrameType): Int =
MAX_SAVED_FRAMES_PER_TYPE
override fun getSaveFrameIdentifier(frame: SavedFrame, metaData: InterimResult): SavedFrameType? {
val hasCard = metaData.analyzerResult.isCardVisible == true
val hasPan = isValidPan(metaData.analyzerResult.ocr?.pan)
return if (hasCard || hasPan) SavedFrameType(hasCard = hasCard, hasPan = hasPan) else null
}
}
override suspend fun aggregateResult(
frame: MainLoopAnalyzer.Input,
result: MainLoopAnalyzer.Prediction
): Pair {
val previousState = state
val currentState = previousState.consumeTransition(result)
state = currentState
val interimResult = InterimResult(
analyzerResult = result,
frame = frame,
state = currentState,
)
val mostLikelyPan = when (currentState) {
is MainLoopState.Initial -> null
is MainLoopState.PanFound -> currentState.getMostLikelyPan()
is MainLoopState.PanSatisfied -> currentState.pan
is MainLoopState.CardSatisfied -> currentState.getMostLikelyPan()
is MainLoopState.Finished -> currentState.pan
}
val savedFrame = SavedFrame(
pan = result.ocr?.pan ?: mostLikelyPan,
frame = frame,
details = FrameDetails(
hasPan = isValidPan(result.ocr?.pan),
panSideConfidence = result.card?.panProbability ?: 0F,
noPanSideConfidence = result.card?.noPanProbability ?: 0F,
noCardConfidence = result.card?.noCardProbability ?: 0F,
),
)
frame.cameraPreviewImage.image.tracker.trackResult("main_loop_aggregated")
if (Config.isDebug) {
Log.d(Config.logTag, "Delay between capture and process of image is ${frame.cameraPreviewImage.image.tracker.startedAt.elapsedSince()}")
}
frameSaver.saveFrame(savedFrame, interimResult)
return if (currentState is MainLoopState.Finished) {
val savedFrames = frameSaver.getSavedFrames()
frameSaver.reset()
interimResult to FinalResult(
pan = currentState.pan,
savedFrames = savedFrames,
averageFrameRate = frameRateTracker.getAverageFrameRate(),
)
} else {
interimResult to null
}
}
override fun reset() {
super.reset()
runBlocking { frameSaver.reset() }
}
}
================================================
FILE: cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/result/MainLoopStateMachine.kt
================================================
package com.getbouncer.cardscan.ui.result
import androidx.annotation.VisibleForTesting
import com.getbouncer.cardscan.ui.analyzer.MainLoopAnalyzer
import com.getbouncer.scan.framework.MachineState
import com.getbouncer.scan.framework.time.seconds
import com.getbouncer.scan.framework.util.ItemTotalCounter
import com.getbouncer.scan.payment.card.isValidPan
@VisibleForTesting
internal val PAN_SEARCH_DURATION = 5.seconds
@VisibleForTesting
internal val PAN_AND_CARD_SEARCH_DURATION = 10.seconds
@VisibleForTesting
internal val DESIRED_PAN_AGREEMENT = 5
@VisibleForTesting
internal val MINIMUM_PAN_AGREEMENT = 2
@VisibleForTesting
internal val DESIRED_SIDE_COUNT = 8
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
sealed class MainLoopState(
val runOcr: Boolean,
val runCardDetect: Boolean,
) : MachineState() {
internal abstract suspend fun consumeTransition(
transition: MainLoopAnalyzer.Prediction,
): MainLoopState
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class Initial : MainLoopState(runOcr = true, runCardDetect = false) {
override suspend fun consumeTransition(
transition: MainLoopAnalyzer.Prediction,
): MainLoopState = when {
isValidPan(transition.ocr?.pan) ->
PanFound(ItemTotalCounter(transition.ocr?.pan ?: ""))
else -> this
}
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class PanFound(
private val panCounter: ItemTotalCounter,
) : MainLoopState(runOcr = true, runCardDetect = true) {
private var visibleCardCount = 0
fun getMostLikelyPan() = panCounter.getHighestCountItem()?.second
private fun isCardSatisfied() = visibleCardCount >= DESIRED_SIDE_COUNT
private fun isPanSatisfied() =
(panCounter.getHighestCountItem()?.first ?: 0) >= DESIRED_PAN_AGREEMENT ||
(
(
panCounter.getHighestCountItem()?.first
?: 0
) >= MINIMUM_PAN_AGREEMENT &&
reachedStateAt.elapsedSince() > PAN_SEARCH_DURATION
)
override suspend fun consumeTransition(
transition: MainLoopAnalyzer.Prediction,
): MainLoopState {
if (isValidPan(transition.ocr?.pan)) {
panCounter.countItem(transition.ocr?.pan ?: "")
}
if (transition.isCardVisible == true) {
visibleCardCount++
}
return when {
reachedStateAt.elapsedSince() > PAN_AND_CARD_SEARCH_DURATION -> Finished(getMostLikelyPan() ?: "")
isCardSatisfied() && isPanSatisfied() -> Finished(getMostLikelyPan() ?: "")
isCardSatisfied() -> CardSatisfied(panCounter)
isPanSatisfied() -> PanSatisfied(getMostLikelyPan() ?: "", visibleCardCount)
else -> this
}
}
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class PanSatisfied(
val pan: String,
var visibleCardCount: Int,
) : MainLoopState(runOcr = false, runCardDetect = true) {
private fun isCardSatisfied() = visibleCardCount >= DESIRED_SIDE_COUNT
override suspend fun consumeTransition(
transition: MainLoopAnalyzer.Prediction,
): MainLoopState {
if (transition.isCardVisible == true) {
visibleCardCount++
}
return when {
reachedStateAt.elapsedSince() > PAN_SEARCH_DURATION -> Finished(pan)
isCardSatisfied() -> Finished(pan)
else -> this
}
}
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class CardSatisfied(
private val panCounter: ItemTotalCounter,
) : MainLoopState(runOcr = true, runCardDetect = false) {
fun getMostLikelyPan() = panCounter.getHighestCountItem()?.second
private fun isPanSatisfied() =
panCounter.getHighestCountItem()?.first ?: 0 >= DESIRED_PAN_AGREEMENT
override suspend fun consumeTransition(
transition: MainLoopAnalyzer.Prediction,
): MainLoopState {
if (transition.ocr?.pan != null && isValidPan(transition.ocr.pan)) {
panCounter.countItem(transition.ocr.pan)
}
return when {
isPanSatisfied() -> Finished(getMostLikelyPan() ?: "")
reachedStateAt.elapsedSince() >= PAN_SEARCH_DURATION ->
Finished(getMostLikelyPan() ?: "")
else -> this
}
}
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class Finished(val pan: String) : MainLoopState(runOcr = false, runCardDetect = false) {
override suspend fun consumeTransition(
transition: MainLoopAnalyzer.Prediction,
): MainLoopState = this
}
}
================================================
FILE: cardscan-ui/src/main/res/values/colors.xml
================================================
#AA000000@android:color/white@android:color/white@android:color/black
================================================
FILE: cardscan-ui/src/main/res/values/dimensions.xml
================================================
24sp24dp16dp18sp
================================================
FILE: cardscan-ui/src/main/res/values/strings.xml
================================================
Processing, please waitEnter card manually
================================================
FILE: cardscan-ui/src/test/java/com/getbouncer/cardscan/ui/result/MainLoopStateMachineTest.kt
================================================
package com.getbouncer.cardscan.ui.result
import androidx.test.filters.LargeTest
import com.getbouncer.cardscan.ui.analyzer.MainLoopAnalyzer
import com.getbouncer.scan.framework.time.delay
import com.getbouncer.scan.framework.time.milliseconds
import com.getbouncer.scan.framework.util.ItemTotalCounter
import com.getbouncer.scan.payment.ml.CardDetect
import com.getbouncer.scan.payment.ml.SSDOcr
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertSame
import kotlin.test.assertTrue
class MainLoopStateMachineTest {
@Test
fun initial_runsOcrOnly() {
val state = MainLoopState.Initial()
assertTrue(state.runOcr)
assertFalse(state.runCardDetect)
}
@Test
@ExperimentalCoroutinesApi
fun initial_noCard_noOcr() = runBlockingTest {
val state = MainLoopState.Initial()
val prediction = MainLoopAnalyzer.Prediction(
ocr = null,
card = null,
)
val newState = state.consumeTransition(prediction)
assertTrue(newState is MainLoopState.Initial)
}
@Test
@ExperimentalCoroutinesApi
fun initial_noCard_foundOcr() = runBlockingTest {
val state = MainLoopState.Initial()
val prediction = MainLoopAnalyzer.Prediction(
ocr = SSDOcr.Prediction(
pan = "4847186095118770",
detectedBoxes = emptyList(),
),
card = null,
)
val newState = state.consumeTransition(prediction)
assertTrue(newState is MainLoopState.PanFound)
assertEquals("4847186095118770", newState.getMostLikelyPan())
}
@Test
fun panFound_runsCardDetectAndOcrOnly() {
val state = MainLoopState.PanFound(
panCounter = ItemTotalCounter("4847186095118770"),
)
assertTrue(state.runOcr)
assertTrue(state.runCardDetect)
}
@Test
@ExperimentalCoroutinesApi
fun panFound_noCard_noTimeout() = runBlockingTest {
val state = MainLoopState.PanFound(
panCounter = ItemTotalCounter("4847186095118770"),
)
val prediction = MainLoopAnalyzer.Prediction(
ocr = SSDOcr.Prediction(
pan = "4847186095118770",
detectedBoxes = emptyList(),
),
card = null,
)
val newState = state.consumeTransition(prediction)
assertTrue(newState is MainLoopState.PanFound)
assertEquals("4847186095118770", newState.getMostLikelyPan())
}
@Test
@ExperimentalCoroutinesApi
fun panFound_cardSatisfied_noTimeout() = runBlockingTest {
var state: MainLoopState = MainLoopState.PanFound(
panCounter = ItemTotalCounter("4847186095118770"),
)
val prediction = MainLoopAnalyzer.Prediction(
ocr = null,
card = CardDetect.Prediction(
side = CardDetect.Prediction.Side.PAN,
panProbability = 1.0F,
noPanProbability = 0.0F,
noCardProbability = 0.0F,
),
)
repeat(DESIRED_SIDE_COUNT - 1) {
state = state.consumeTransition(prediction)
assertTrue(state is MainLoopState.PanFound)
}
val newState = state.consumeTransition(prediction)
assertTrue(newState is MainLoopState.CardSatisfied)
assertEquals("4847186095118770", newState.getMostLikelyPan())
}
@Test
@ExperimentalCoroutinesApi
fun panFound_panSatisfied_noTimeout() = runBlockingTest {
var state: MainLoopState = MainLoopState.PanFound(
panCounter = ItemTotalCounter("4847186095118770"),
)
val prediction = MainLoopAnalyzer.Prediction(
ocr = SSDOcr.Prediction(
pan = "4847186095118770",
detectedBoxes = emptyList(),
),
card = null,
)
repeat(DESIRED_PAN_AGREEMENT - 2) {
state = state.consumeTransition(prediction)
assertTrue(state is MainLoopState.PanFound)
}
val newState = state.consumeTransition(prediction)
assertTrue(newState is MainLoopState.PanSatisfied)
assertEquals("4847186095118770", newState.pan)
}
/**
* This test cannot use `runBlockingTest` because it requires a delay. While runBlockingTest
* advances the dispatcher's virtual time by the specified amount, it does not affect the timing
* of the duration.
*/
@Test
@ExperimentalCoroutinesApi
fun panFound_panSatisfied_timeout() = runBlocking {
var state: MainLoopState = MainLoopState.PanFound(
panCounter = ItemTotalCounter("4847186095118770"),
)
val prediction = MainLoopAnalyzer.Prediction(
ocr = SSDOcr.Prediction(
pan = "4847186095118770",
detectedBoxes = emptyList(),
),
card = null,
)
repeat(MINIMUM_PAN_AGREEMENT - 2) {
state = state.consumeTransition(prediction)
assertTrue(state is MainLoopState.PanFound)
}
delay(PAN_SEARCH_DURATION + 1.milliseconds)
val newState = state.consumeTransition(prediction)
assertTrue(newState is MainLoopState.PanSatisfied)
assertEquals("4847186095118770", newState.pan)
}
/**
* This test cannot use `runBlockingTest` because it requires a delay. While runBlockingTest
* advances the dispatcher's virtual time by the specified amount, it does not affect the timing
* of the duration.
*/
@Test
@LargeTest
fun panFound_timeout() = runBlocking {
val state = MainLoopState.PanFound(
panCounter = ItemTotalCounter("4847186095118770"),
)
delay(PAN_AND_CARD_SEARCH_DURATION + 1.milliseconds)
val prediction = MainLoopAnalyzer.Prediction(
ocr = null,
card = null,
)
val newState = state.consumeTransition(prediction)
assertTrue(newState is MainLoopState.Finished, "$newState is not Finished")
assertEquals("4847186095118770", newState.pan)
}
@Test
fun panSatisfied_runsCardDetectOnly() {
val state = MainLoopState.PanSatisfied(
pan = "4847186095118770",
visibleCardCount = 0,
)
assertFalse(state.runOcr)
assertTrue(state.runCardDetect)
}
@Test
@ExperimentalCoroutinesApi
fun panSatisfied_noCard_noTimeout() = runBlockingTest {
val state = MainLoopState.PanSatisfied(
pan = "4847186095118770",
visibleCardCount = 0,
)
val prediction = MainLoopAnalyzer.Prediction(
ocr = null,
card = CardDetect.Prediction(
side = CardDetect.Prediction.Side.PAN,
panProbability = 1.0F,
noPanProbability = 0.0F,
noCardProbability = 0.0F,
),
)
val newState = state.consumeTransition(prediction)
assertTrue(newState is MainLoopState.PanSatisfied)
assertEquals("4847186095118770", newState.pan)
}
@Test
@ExperimentalCoroutinesApi
fun panSatisfied_enoughSides_noTimeout() = runBlockingTest {
val state = MainLoopState.PanSatisfied(
pan = "4847186095118770",
visibleCardCount = DESIRED_SIDE_COUNT - 1,
)
val prediction = MainLoopAnalyzer.Prediction(
ocr = null,
card = CardDetect.Prediction(
side = CardDetect.Prediction.Side.PAN,
panProbability = 1.0F,
noPanProbability = 0.0F,
noCardProbability = 0.0F,
),
)
val newState = state.consumeTransition(prediction)
assertTrue(newState is MainLoopState.Finished)
assertEquals("4847186095118770", newState.pan)
}
/**
* This test cannot use `runBlockingTest` because it requires a delay. While runBlockingTest
* advances the dispatcher's virtual time by the specified amount, it does not affect the timing
* of the duration.
*/
@Test
@LargeTest
fun panSatisfied_timeout() = runBlocking {
val state = MainLoopState.PanSatisfied(
pan = "4847186095118770",
visibleCardCount = DESIRED_SIDE_COUNT - 1,
)
val prediction = MainLoopAnalyzer.Prediction(
ocr = null,
card = null,
)
delay(PAN_SEARCH_DURATION + 1.milliseconds)
val newState = state.consumeTransition(prediction)
assertTrue(newState is MainLoopState.Finished)
assertEquals("4847186095118770", newState.pan)
}
@Test
fun cardSatisfied_runsOcrOnly() {
val state = MainLoopState.CardSatisfied(
panCounter = ItemTotalCounter("4847186095118770"),
)
assertTrue(state.runOcr)
assertFalse(state.runCardDetect)
}
@Test
@ExperimentalCoroutinesApi
fun cardSatisfied_noPan_noTimeout() = runBlockingTest {
val state = MainLoopState.CardSatisfied(
panCounter = ItemTotalCounter("4847186095118770"),
)
val prediction = MainLoopAnalyzer.Prediction(
ocr = SSDOcr.Prediction(
pan = "4847186095118770",
detectedBoxes = emptyList(),
),
card = null,
)
val newState = state.consumeTransition(prediction)
assertTrue(newState is MainLoopState.CardSatisfied)
assertEquals("4847186095118770", newState.getMostLikelyPan())
}
@Test
@ExperimentalCoroutinesApi
fun cardSatisfied_pan_noTimeout() = runBlockingTest {
var state: MainLoopState = MainLoopState.CardSatisfied(
panCounter = ItemTotalCounter("4847186095118770"),
)
val prediction = MainLoopAnalyzer.Prediction(
ocr = SSDOcr.Prediction(
pan = "4847186095118770",
detectedBoxes = emptyList(),
),
card = null,
)
repeat(DESIRED_PAN_AGREEMENT - 2) {
state = state.consumeTransition(prediction)
assertTrue(state is MainLoopState.CardSatisfied)
}
val newState = state.consumeTransition(prediction)
assertTrue(newState is MainLoopState.Finished)
assertEquals("4847186095118770", newState.pan)
}
/**
* This test cannot use `runBlockingTest` because it requires a delay. While runBlockingTest
* advances the dispatcher's virtual time by the specified amount, it does not affect the timing
* of the duration.
*/
@Test
@LargeTest
fun cardSatisfied_noPan_timeout() = runBlocking {
val state = MainLoopState.CardSatisfied(
panCounter = ItemTotalCounter("4847186095118770"),
)
val prediction = MainLoopAnalyzer.Prediction(
ocr = null,
card = null,
)
delay(PAN_SEARCH_DURATION + 1.milliseconds)
val newState = state.consumeTransition(prediction)
assertTrue(newState is MainLoopState.Finished)
assertEquals("4847186095118770", newState.pan)
}
@Test
fun finished_runsNothing() {
val state = MainLoopState.Finished(
pan = "4847186095118770",
)
assertFalse(state.runOcr)
assertFalse(state.runCardDetect)
}
@Test
@ExperimentalCoroutinesApi
fun finished_goesNowhere() = runBlockingTest {
val state = MainLoopState.Finished(
pan = "4847186095118770",
)
val prediction = MainLoopAnalyzer.Prediction(
ocr = SSDOcr.Prediction(
pan = "4847186095118770",
detectedBoxes = emptyList(),
),
card = CardDetect.Prediction(
side = CardDetect.Prediction.Side.NO_CARD,
panProbability = 0.0F,
noPanProbability = 0.0F,
noCardProbability = 1.0F,
),
)
val newState = state.consumeTransition(prediction)
assertSame(state, newState)
}
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Fri Feb 26 15:43:08 PST 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-all.zip
================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
version=2.2.0003
================================================
FILE: gradlew
================================================
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: scan-camera/.gitignore
================================================
/build
================================================
FILE: scan-camera/README.md
================================================
# Deprecation Notice
Hello from the Stripe (formerly Bouncer) team!
We'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.
This 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!
If 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.
If 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).
For the new product, please visit the [stripe github repository](https://github.com/stripe/stripe-android/tree/master/stripecardscan).
# Overview
This 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.
Note 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.

## Contents
* [Requirements](#requirements)
* [Demo](#demo)
* [Integration](#integration)
* [Using](#using)
* [Developing](#developing)
* [Authors](#authors)
* [License](#license)
## Requirements
* Android API level 21 or higher
* Kotlin coroutine compatibility
Note: Your app does not have to be written in kotlin to integrate scan-camera, but must be able to depend on kotlin functionality.
## Demo
An app demonstrating the basic capabilities of CardScan is available in [github](https://github.com/getbouncer/cardscan-demo-android).
## Integration
See the [integration documentation](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) in the Bouncer Docs.
## Using
This 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.
For 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).
### Getting images from the camera
See the [example code](https://docs.getbouncer.com/card-scan/android-integration-guide/android-architecture-overview#example) in the Android architecture documentation.
## Developing
See the [development docs](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) for details on developing this library.
## Authors
Adam Wushensky, Sam King, and Zain ul Abi Din
## License
This library is available under the MIT license. See the [LICENSE](../LICENSE) file for the full license text.
================================================
FILE: scan-camera/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 30
buildToolsVersion '30.0.3'
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
testOptions {
unitTests.includeAndroidResources = true
}
lintOptions {
enable "Interoperability"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":scan-framework")
implementation "androidx.appcompat:appcompat:[1.3.0,1.3.1]"
implementation "androidx.core:core-ktx:[1.3.1,1.6.0]"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:[1.4.0,1.5.1]"
}
dependencies {
testImplementation "androidx.test:core:1.4.0"
testImplementation "androidx.test:runner:1.4.0"
testImplementation "junit:junit:4.13.2"
testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1"
}
dependencies {
androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30"
}
apply from: 'deploy.gradle'
================================================
FILE: scan-camera/consumer-rules.pro
================================================
================================================
FILE: scan-camera/deploy.gradle
================================================
apply plugin: 'maven-publish'
apply plugin: 'org.jetbrains.dokka'
apply plugin: 'signing'
task androidSourcesJar(type: Jar) {
archiveClassifier.set('sources')
if (project.plugins.findPlugin("com.android.library")) {
// Android library
from android.sourceSets.main.java.srcDirs
from android.sourceSets.main.kotlin.srcDirs
} else {
// Pure kotlin library
from sourceSets.main.java.srcDirs
from sourceSets.main.kotlin.srcDirs
}
}
tasks.withType(dokkaHtmlPartial.getClass()).configureEach {
pluginsMapConfiguration.set(
["org.jetbrains.dokka.base.DokkaBase": """{ "separateInheritedMembers": true}"""]
)
}
task javadocJar(type: Jar, dependsOn: dokkaJavadoc) {
archiveClassifier.set('javadoc')
from dokkaJavadoc.outputDirectory
}
artifacts {
archives androidSourcesJar
archives javadocJar
}
ext["signing.keyId"] = ''
ext["signing.password"] = ''
ext["signing.secretKeyRingFile"] = ''
ext["ossrhUsername"] = ''
ext["ossrhPassword"] = ''
ext["sonatypeStagingProfileId"] = ''
ext {
libraryDescription = 'This library provides the framework for cameras'
siteUrl = 'https://getbouncer.com'
scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'
scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'
scmUrl = 'https://github.com/getbouncer/cardscan-android'
licenseName = 'bouncer-free-1'
licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'
developerId = 'getbouncer'
developerName = 'Bouncer Technologies'
developerEmail = 'bouncer-support@stripe.com'
publishGroupId = 'com.getbouncer'
publishArtifactId = 'scan-camera'
publishVersion = version
}
group = publishGroupId
version = publishVersion
File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
Properties p = new Properties()
new FileInputStream(secretPropsFile).withCloseable { is ->
p.load(is)
}
p.each { name, value ->
ext[name] = value
}
} else {
ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')
ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
}
publishing {
publications {
release(MavenPublication) {
groupId publishGroupId
artifactId publishArtifactId
version publishVersion
// Two artifacts, the `aar` (or `jar`) and the sources
if (project.plugins.findPlugin("com.android.library")) {
artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
} else {
artifact("$buildDir/libs/${project.getName()}-${version}.jar")
}
artifact androidSourcesJar
pom {
name = publishArtifactId
description = libraryDescription
url = siteUrl
licenses {
license {
name = licenseName
url = licenseUrl
}
}
developers {
developer {
id = developerId
name = developerName
email = developerEmail
}
}
scm {
connection = scmConnection
developerConnection = scmDeveloperConnection
url = scmUrl
}
// A slightly hacky fix so that your POM will include any transitive dependencies
// that your library builds upon
withXml {
def dependenciesNode = asNode().appendNode('dependencies')
project.configurations.implementation.allDependencies.each {
if (it.group != null && it.version != null) {
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', it.group)
dependencyNode.appendNode('artifactId', it.name)
dependencyNode.appendNode('version', it.version)
}
}
}
}
}
}
// The repository to publish to, Sonatype/MavenCentral
repositories {
maven {
// This is an arbitrary name, you may also use "mavencentral" or
// any other name that's descriptive for you
name = "sonatype"
url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
credentials {
username ossrhUsername
password ossrhPassword
}
}
}
}
signing {
sign publishing.publications
}
================================================
FILE: scan-camera/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: scan-camera/src/main/AndroidManifest.xml
================================================
================================================
FILE: scan-camera/src/main/java/com/getbouncer/scan/camera/Camera1Adapter.kt
================================================
@file:Suppress("deprecation")
package com.getbouncer.scan.camera
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageFormat
import android.graphics.PointF
import android.graphics.Rect
import android.hardware.Camera
import android.hardware.Camera.AutoFocusCallback
import android.hardware.Camera.PreviewCallback
import android.os.Handler
import android.os.HandlerThread
import android.util.DisplayMetrics
import android.util.Log
import android.util.Size
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.ViewGroup
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.OnLifecycleEvent
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.framework.Stats
import com.getbouncer.scan.framework.TrackedImage
import com.getbouncer.scan.framework.image.NV21Image
import com.getbouncer.scan.framework.image.getRenderScript
import com.getbouncer.scan.framework.image.rotate
import com.getbouncer.scan.framework.util.retrySync
import java.lang.ref.WeakReference
import java.util.ArrayList
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
private const val ASPECT_TOLERANCE = 0.2
private val MAXIMUM_RESOLUTION = Size(1920, 1080)
/**
* A [CameraAdapter] that uses android's Camera 1 APIs to show previews and process images.
*/
internal class Camera1Adapter(
private val activity: Activity,
private val previewView: ViewGroup,
private val minimumResolution: Size,
private val cameraErrorListener: CameraErrorListener,
) : CameraAdapter>(), PreviewCallback {
override val implementationName: String = "Camera1"
private var mCamera: Camera? = null
private var cameraPreview: CameraPreview? = null
private var mRotation = 0
private var onCameraAvailableListener: WeakReference<((Camera) -> Unit)?> = WeakReference(null)
private var currentCameraId = 0
private val mainThreadHandler = Handler(activity.mainLooper)
private var cameraThread: HandlerThread? = null
private var cameraHandler: Handler? = null
override fun withFlashSupport(task: (Boolean) -> Unit) {
mCamera?.let {
task(isFlashSupported(it))
} ?: run {
onCameraAvailableListener = WeakReference { cam ->
task(isFlashSupported(cam))
}
}
}
private fun isFlashSupported(camera: Camera) =
camera.parameters?.supportedFlashModes?.contains(Camera.Parameters.FLASH_MODE_TORCH) == true
override fun setTorchState(on: Boolean) {
mCamera?.apply {
val parameters = parameters
if (on) {
parameters.flashMode = Camera.Parameters.FLASH_MODE_TORCH
} else {
parameters.flashMode = Camera.Parameters.FLASH_MODE_OFF
}
setCameraParameters(this, parameters)
startCameraPreview()
}
}
override fun isTorchOn(): Boolean =
mCamera?.parameters?.flashMode == Camera.Parameters.FLASH_MODE_TORCH
override fun setFocus(point: PointF) {
mCamera?.apply {
val params = parameters
if (params.maxNumFocusAreas > 0) {
val focusRect = Rect(
point.x.toInt() - 150,
point.y.toInt() - 150,
point.x.toInt() + 150,
point.y.toInt() + 150
)
val cameraFocusAreas: MutableList = ArrayList()
cameraFocusAreas.add(Camera.Area(focusRect, 1000))
params.focusAreas = cameraFocusAreas
setCameraParameters(this, params)
}
}
}
override fun onPreviewFrame(bytes: ByteArray?, camera: Camera) {
// this method may be called after the camera has closed if there was still an image in
// flight. In this case, swallow the error. Ideally, we would be able to tell whether the
// exception was due to the camera already having been closed or from an error with camera
// hardware.
val imageWidth = try { camera.parameters.previewSize.width } catch (t: Throwable) { return }
val imageHeight = try { camera.parameters.previewSize.height } catch (t: Throwable) { return }
if (bytes != null) {
try {
sendImageToStream(
CameraPreviewImage(
TrackedImage(
image = NV21Image(imageWidth, imageHeight, bytes)
.toBitmap(getRenderScript(activity))
.rotate(mRotation.toFloat()),
tracker = Stats.trackRepeatingTask("image_processing")
),
Rect(0, 0, previewView.width, previewView.height),
),
)
} catch (t: Throwable) {
// ignore errors transforming the image (OOM, etc)
Log.e(Config.logTag, "Exception caught during camera transform", t)
} finally {
camera.addCallbackBuffer(bytes)
}
} else {
camera.addCallbackBuffer(ByteArray((imageWidth * imageHeight * 1.5).roundToInt()))
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
override fun onPause() {
super.onPause()
mCamera?.stopPreview()
mCamera?.setPreviewCallbackWithBuffer(null)
mCamera?.release()
mCamera = null
cameraPreview?.apply { holder.removeCallback(this) }
cameraPreview = null
stopCameraThread()
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
startCameraThread()
mainThreadHandler.post {
try {
var camera: Camera? = null
try {
camera = Camera.open(currentCameraId)
} catch (t: Throwable) {
cameraErrorListener.onCameraOpenError(t)
}
onCameraOpen(camera)
} catch (t: Throwable) {
cameraErrorListener.onCameraOpenError(t)
}
}
}
/**
* Starts a background thread and its [Handler].
*/
private fun startCameraThread() {
val thread = HandlerThread("CameraBackground").also { it.start() }
cameraThread = thread
cameraHandler = Handler(thread.looper)
}
/**
* Stops the background thread and its [Handler].
*/
private fun stopCameraThread() {
cameraThread?.quitSafely()
try {
cameraThread?.join()
cameraThread = null
cameraHandler = null
} catch (e: InterruptedException) {
mainThreadHandler.post { cameraErrorListener.onCameraOpenError(e) }
}
}
private fun setCameraParameters(
camera: Camera,
parameters: Camera.Parameters
) {
try {
camera.parameters = parameters
} catch (t: Throwable) {
Log.w(Config.logTag, "Error setting camera parameters", t)
// ignore failure to set camera parameters
}
}
private fun startCameraPreview() {
cameraHandler?.post {
try {
retrySync(times = 5) {
mCamera?.startPreview()
}
} catch (t: Throwable) {
mainThreadHandler.post {
cameraErrorListener.onCameraOpenError(t)
}
}
}
}
private fun onCameraOpen(camera: Camera?) {
if (camera == null) {
mainThreadHandler.post {
cameraPreview?.apply { holder.removeCallback(this) }
cameraErrorListener.onCameraOpenError(null)
}
} else {
mCamera = camera
setCameraDisplayOrientation(activity)
setCameraPreviewFrame()
// Create our Preview view and set it as the content of our activity.
cameraPreview = CameraPreview(activity, this).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}.also { cameraPreview ->
mainThreadHandler.post {
onCameraAvailableListener.get()?.let {
it(camera)
}
onCameraAvailableListener.clear()
previewView.removeAllViews()
previewView.addView(cameraPreview)
}
}
}
}
private fun setCameraPreviewFrame() {
mCamera?.apply {
val format = ImageFormat.NV21
val parameters = parameters
parameters.previewFormat = format
val displayMetrics = DisplayMetrics()
activity.windowManager.defaultDisplay.getRealMetrics(displayMetrics)
val displayWidth = max(displayMetrics.heightPixels, displayMetrics.widthPixels)
val displayHeight = min(displayMetrics.heightPixels, displayMetrics.widthPixels)
val height: Int = minimumResolution.height
val width = displayWidth * height / displayHeight
getOptimalPreviewSize(parameters.supportedPreviewSizes, width, height)?.apply {
parameters.setPreviewSize(this.width, this.height)
}
setCameraParameters(this, parameters)
}
}
private fun getOptimalPreviewSize(
sizes: List?,
w: Int,
h: Int
): Camera.Size? {
val targetRatio = w.toDouble() / h
if (sizes == null) {
return null
}
var optimalSize: Camera.Size? = null
// Find the smallest size that fits our tolerance and is at least as big as our target
// height
for (size in sizes) {
val ratio = size.width.toDouble() / size.height
if (abs(ratio - targetRatio) <= ASPECT_TOLERANCE) {
if (size.height >= h) {
optimalSize = size
}
}
}
// Find the closest ratio that is still taller than our target height
if (optimalSize == null) {
var minDiffRatio = Double.MAX_VALUE
for (size in sizes) {
val ratio = size.width.toDouble() / size.height
val ratioDiff = abs(ratio - targetRatio)
if (size.height >= h && ratioDiff <= minDiffRatio &&
size.height <= MAXIMUM_RESOLUTION.height && size.width <= MAXIMUM_RESOLUTION.width
) {
optimalSize = size
minDiffRatio = ratioDiff
}
}
}
if (optimalSize == null) {
// Find the smallest size that is at least as big as our target height
for (size in sizes) {
if (size.height >= h) {
optimalSize = size
}
}
}
return optimalSize
}
private fun setCameraDisplayOrientation(activity: Activity) {
val camera = mCamera ?: return
val info = Camera.CameraInfo()
Camera.getCameraInfo(Camera.CameraInfo.CAMERA_FACING_BACK, info)
val rotation = activity.windowManager.defaultDisplay.rotation
val degrees = rotation.rotationToDegrees()
val result = if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
(360 - (info.orientation + degrees) % 360) % 360 // compensate for the mirror
} else { // back-facing
(info.orientation - degrees + 360) % 360
}
try {
camera.stopPreview()
} catch (e: java.lang.Exception) {
// preview was already stopped
}
try {
camera.setDisplayOrientation(result)
} catch (t: Throwable) {
// cameraErrorListener.onCameraUnsupportedError(t)
}
startCameraPreview()
mRotation = result
}
/** A basic Camera preview class */
@SuppressLint("ViewConstructor")
private inner class CameraPreview(
context: Context,
private val mPreviewCallback: PreviewCallback
) : SurfaceView(context), AutoFocusCallback, SurfaceHolder.Callback {
init {
holder.addCallback(this)
mCamera?.apply {
val params = parameters
val focusModes = params.supportedFocusModes
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
params.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE
} else if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
params.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO
}
params.setRecordingHint(true)
setCameraParameters(this, params)
}
}
override fun onAutoFocus(success: Boolean, camera: Camera) {}
/**
* The Surface has been created, now tell the camera where to draw the preview.
*/
override fun surfaceCreated(holder: SurfaceHolder) {
try {
mCamera?.setPreviewDisplay(this.holder)
mCamera?.setPreviewCallbackWithBuffer(mPreviewCallback)
startCameraPreview()
} catch (t: Throwable) {
mainThreadHandler.post {
cameraErrorListener.onCameraOpenError(t)
}
}
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// empty. Take care of releasing the Camera preview in your activity.
}
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
w: Int,
h: Int
) {
// If your preview can change or rotate, take care of those events here.
// Make sure to stop the preview before resizing or reformatting it.
if (this.holder.surface == null) {
// preview surface does not exist
return
}
// stop preview before making changes
try {
mCamera?.stopPreview()
} catch (t: Throwable) {
// ignore: tried to stop a non-existent preview
}
// set preview size and make any resize, rotate or
// reformatting changes here
// start preview with new settings
try {
mCamera?.setPreviewDisplay(this.holder)
val bufSize = w * h * ImageFormat.getBitsPerPixel(format) / 8
for (i in 0..2) {
mCamera?.addCallbackBuffer(ByteArray(bufSize))
}
mCamera?.setPreviewCallbackWithBuffer(mPreviewCallback)
startCameraPreview()
} catch (t: Throwable) {
mainThreadHandler.post {
cameraErrorListener.onCameraOpenError(t)
}
}
}
}
override fun withSupportsMultipleCameras(task: (Boolean) -> Unit) {
task(Camera.getNumberOfCameras() > 1)
}
override fun changeCamera() {
currentCameraId++
if (currentCameraId >= Camera.getNumberOfCameras()) {
currentCameraId = 0
}
onPause()
onResume()
}
override fun getCurrentCamera(): Int = currentCameraId
}
================================================
FILE: scan-camera/src/main/java/com/getbouncer/scan/camera/CameraAdapter.kt
================================================
package com.getbouncer.scan.camera
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.PointF
import android.graphics.Rect
import android.util.Log
import android.view.Surface
import androidx.annotation.CheckResult
import androidx.annotation.IntDef
import androidx.annotation.MainThread
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.framework.TrackedImage
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedSendChannelException
// TODO: upgrade this when kotlin libs hit 1.5.0
// import kotlinx.coroutines.channels.onClosed
// import kotlinx.coroutines.channels.onFailure
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.runBlocking
/**
* Valid integer rotation values.
*/
@IntDef(
Surface.ROTATION_0,
Surface.ROTATION_90,
Surface.ROTATION_180,
Surface.ROTATION_270
)
@Retention(AnnotationRetention.SOURCE)
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
annotation class RotationValue
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class CameraPreviewImage(
val image: TrackedImage,
val previewImageBounds: Rect,
)
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
abstract class CameraAdapter : LifecycleObserver {
// TODO: change this to be a channelFlow once it's no longer experimental, add some capacity and use a backpressure drop strategy
private val imageChannel = Channel(capacity = Channel.RENDEZVOUS)
private var lifecyclesBound = 0
abstract val implementationName: String
companion object {
/**
* Determine if the device supports the camera features used by this SDK.
*/
@JvmStatic
fun isCameraSupported(context: Context): Boolean =
(context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)).also {
if (!it) Log.e(Config.logTag, "System feature 'FEATURE_CAMERA_ANY' is unavailable")
}
/**
* Calculate degrees from a [RotationValue].
*/
@CheckResult
fun Int.rotationToDegrees(): Int = this * 90
}
protected fun sendImageToStream(image: CameraOutput) = try {
// TODO: upgrade this when kotlin libs hit 1.5.0
// imageChannel.trySend(image).onClosed {
// Log.w(Config.logTag, "Attempted to send image to closed channel", it)
// }.onFailure {
// Log.w(Config.logTag, "Failure when sending image to channel", it)
// }
imageChannel.offer(image)
} catch (e: ClosedSendChannelException) {
Log.w(Config.logTag, "Attempted to send image to closed channel")
} catch (t: Throwable) {
Log.e(Config.logTag, "Unable to send image to channel", t)
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroyed() {
runBlocking { imageChannel.close() }
}
/**
* Bind this camera manager to a lifecycle.
*/
open fun bindToLifecycle(lifecycleOwner: LifecycleOwner) {
lifecycleOwner.lifecycle.addObserver(this)
lifecyclesBound++
}
/**
* Unbind this camera from a lifecycle. This will pause the camera.
*/
open fun unbindFromLifecycle(lifecycleOwner: LifecycleOwner) {
lifecycleOwner.lifecycle.removeObserver(this)
lifecyclesBound--
if (lifecyclesBound < 0) {
Log.e(Config.logTag, "Bound lifecycle count $lifecyclesBound is below 0")
lifecyclesBound = 0
}
this.onPause()
}
/**
* Determine if the adapter is currently bound.
*/
open fun isBoundToLifecycle() = lifecyclesBound > 0
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
open fun onPause() {
// support OnPause events.
}
/**
* Execute a task with flash support.
*/
abstract fun withFlashSupport(task: (Boolean) -> Unit)
/**
* Turn the camera torch on or off.
*/
abstract fun setTorchState(on: Boolean)
/**
* Determine if the torch is currently on.
*/
abstract fun isTorchOn(): Boolean
/**
* Determine if the device has multiple cameras.
*/
abstract fun withSupportsMultipleCameras(task: (Boolean) -> Unit)
/**
* Change to a new camera.
*/
abstract fun changeCamera()
/**
* Determine which camera is currently in use.
*/
abstract fun getCurrentCamera(): Int
/**
* Set the focus on a particular point on the screen.
*/
abstract fun setFocus(point: PointF)
/**
* Get the stream of images from the camera. This is a hot [Flow] of images with a back pressure strategy DROP.
* Images that are not read from the flow are dropped. This flow is backed by a [Channel].
*/
fun getImageStream(): Flow = imageChannel.receiveAsFlow()
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
interface CameraErrorListener {
@MainThread
fun onCameraOpenError(cause: Throwable?)
@MainThread
fun onCameraAccessError(cause: Throwable?)
@MainThread
fun onCameraUnsupportedError(cause: Throwable?)
}
================================================
FILE: scan-camera/src/main/java/com/getbouncer/scan/camera/CameraSelector.kt
================================================
package com.getbouncer.scan.camera
import android.app.Activity
import android.graphics.Bitmap
import android.os.Build
import android.util.Log
import android.util.Size
import android.view.ViewGroup
import com.getbouncer.scan.framework.Config
/**
* Get the appropriate camera adapter. If the customer has provided an additional camera adapter, use that in place of
* camera 1.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun getCameraAdapter(
activity: Activity,
previewView: ViewGroup,
minimumResolution: Size,
cameraErrorListener: CameraErrorListener,
): CameraAdapter> =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
try {
getAlternateCamera(activity, previewView, minimumResolution, cameraErrorListener)
} catch (t: Throwable) {
Log.d(Config.logTag, "No alternative camera implementations supplied, falling back to default", t)
Camera1Adapter(activity, previewView, minimumResolution, cameraErrorListener)
}
} else {
Log.d(Config.logTag, "YUV_420_888 is not supported, falling back to default implementation")
Camera1Adapter(activity, previewView, minimumResolution, cameraErrorListener)
}.apply {
Log.d(Config.logTag, "Using camera implementation ${this.implementationName}")
}
@Suppress("UNCHECKED_CAST")
@Throws(ClassNotFoundException::class, NoSuchMethodException::class, IllegalAccessException::class)
private fun getAlternateCamera(
activity: Activity,
previewView: ViewGroup,
minimumResolution: Size,
cameraErrorListener: CameraErrorListener,
): CameraAdapter> =
Class.forName("com.getbouncer.scan.camera.extension.CameraAdapterImpl")
.getConstructor(
Activity::class.java,
ViewGroup::class.java,
Size::class.java,
CameraErrorListener::class.java,
)
.newInstance(
activity,
previewView,
minimumResolution,
cameraErrorListener,
) as CameraAdapter>
================================================
FILE: scan-camera2/.gitignore
================================================
/build
================================================
FILE: scan-camera2/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 30
buildToolsVersion '30.0.3'
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
testOptions {
unitTests.includeAndroidResources = true
}
lintOptions {
enable "Interoperability"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":scan-framework")
implementation project(':scan-camera')
implementation "androidx.appcompat:appcompat:[1.3.0,1.3.1]"
implementation "androidx.core:core-ktx:[1.3.1,1.6.0]"
}
dependencies {
testImplementation "androidx.test:core:1.4.0"
testImplementation "androidx.test:runner:1.4.0"
testImplementation "junit:junit:4.13.2"
testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30"
}
dependencies {
androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30"
}
apply from: 'deploy.gradle'
================================================
FILE: scan-camera2/deploy.gradle
================================================
apply plugin: 'maven-publish'
apply plugin: 'org.jetbrains.dokka'
apply plugin: 'signing'
task androidSourcesJar(type: Jar) {
archiveClassifier.set('sources')
if (project.plugins.findPlugin("com.android.library")) {
// Android library
from android.sourceSets.main.java.srcDirs
from android.sourceSets.main.kotlin.srcDirs
} else {
// Pure kotlin library
from sourceSets.main.java.srcDirs
from sourceSets.main.kotlin.srcDirs
}
}
tasks.withType(dokkaHtmlPartial.getClass()).configureEach {
pluginsMapConfiguration.set(
["org.jetbrains.dokka.base.DokkaBase": """{ "separateInheritedMembers": true}"""]
)
}
task javadocJar(type: Jar, dependsOn: dokkaJavadoc) {
archiveClassifier.set('javadoc')
from dokkaJavadoc.outputDirectory
}
artifacts {
archives androidSourcesJar
archives javadocJar
}
ext["signing.keyId"] = ''
ext["signing.password"] = ''
ext["signing.secretKeyRingFile"] = ''
ext["ossrhUsername"] = ''
ext["ossrhPassword"] = ''
ext["sonatypeStagingProfileId"] = ''
ext {
libraryDescription = 'This library provides the framework for cameras'
siteUrl = 'https://getbouncer.com'
scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'
scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'
scmUrl = 'https://github.com/getbouncer/cardscan-android'
licenseName = 'bouncer-free-1'
licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'
developerId = 'getbouncer'
developerName = 'Bouncer Technologies'
developerEmail = 'bouncer-support@stripe.com'
publishGroupId = 'com.getbouncer'
publishArtifactId = 'scan-camera2'
publishVersion = version
}
group = publishGroupId
version = publishVersion
File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
Properties p = new Properties()
new FileInputStream(secretPropsFile).withCloseable { is ->
p.load(is)
}
p.each { name, value ->
ext[name] = value
}
} else {
ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')
ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
}
publishing {
publications {
release(MavenPublication) {
groupId publishGroupId
artifactId publishArtifactId
version publishVersion
// Two artifacts, the `aar` (or `jar`) and the sources
if (project.plugins.findPlugin("com.android.library")) {
artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
} else {
artifact("$buildDir/libs/${project.getName()}-${version}.jar")
}
artifact androidSourcesJar
pom {
name = publishArtifactId
description = libraryDescription
url = siteUrl
licenses {
license {
name = licenseName
url = licenseUrl
}
}
developers {
developer {
id = developerId
name = developerName
email = developerEmail
}
}
scm {
connection = scmConnection
developerConnection = scmDeveloperConnection
url = scmUrl
}
// A slightly hacky fix so that your POM will include any transitive dependencies
// that your library builds upon
withXml {
def dependenciesNode = asNode().appendNode('dependencies')
project.configurations.implementation.allDependencies.each {
if (it.group != null && it.version != null) {
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', it.group)
dependencyNode.appendNode('artifactId', it.name)
dependencyNode.appendNode('version', it.version)
}
}
}
}
}
}
// The repository to publish to, Sonatype/MavenCentral
repositories {
maven {
// This is an arbitrary name, you may also use "mavencentral" or
// any other name that's descriptive for you
name = "sonatype"
url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
credentials {
username ossrhUsername
password ossrhPassword
}
}
}
}
signing {
sign publishing.publications
}
================================================
FILE: scan-camera2/src/androidTest/java/com/getbouncer/scan/camera/extension/UtilInstrumentationTest.kt
================================================
package com.getbouncer.scan.camera.extension
import android.util.Size
import android.view.Surface
import androidx.test.filters.SmallTest
import org.junit.Test
import kotlin.test.assertEquals
class UtilInstrumentationTest {
@Test
@SmallTest
fun resolutionToSize_perpendicular() {
assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 90))
assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 270))
assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 0))
assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 180))
assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 90))
assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 270))
assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 0))
assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 180))
}
@Test
@SmallTest
fun resolutionToSize_parallel() {
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 0))
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 180))
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 90))
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 270))
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 0))
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 180))
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 90))
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 270))
}
@Test
@SmallTest
fun resolutionToSize_oblique() {
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 45))
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 135))
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 45))
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 135))
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 45))
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 135))
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 45))
assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 135))
}
}
================================================
FILE: scan-camera2/src/main/AndroidManifest.xml
================================================
================================================
FILE: scan-camera2/src/main/java/com/getbouncer/scan/camera/extension/CameraAdapterImpl.kt
================================================
/*
* Copyright 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.getbouncer.scan.camera.extension
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageFormat
import android.graphics.Matrix
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.SurfaceTexture
import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.TotalCaptureResult
import android.hardware.camera2.params.MeteringRectangle
import android.hardware.camera2.params.StreamConfigurationMap
import android.media.ImageReader
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.util.Size
import android.view.Surface
import android.view.TextureView
import android.view.ViewGroup
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import com.getbouncer.scan.camera.CameraAdapter
import com.getbouncer.scan.camera.CameraErrorListener
import com.getbouncer.scan.camera.CameraPreviewImage
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.framework.Stats
import com.getbouncer.scan.framework.TrackedImage
import com.getbouncer.scan.framework.image.getRenderScript
import com.getbouncer.scan.framework.image.isSupportedFormat
import com.getbouncer.scan.framework.image.rotate
import com.getbouncer.scan.framework.image.toBitmap
import com.getbouncer.scan.framework.util.scale
import com.getbouncer.scan.framework.util.scaleAndCenterSurrounding
import com.getbouncer.scan.framework.util.size
import com.getbouncer.scan.framework.util.toRectF
import java.util.Locale
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.max
/**
* For tap to focus.
*/
private const val FOCUS_TOUCH_SIZE = 150
/**
* The default image format. This is not necessarily the fastest to process, but most supported.
*/
const val DEFAULT_IMAGE_FORMAT = ImageFormat.YUV_420_888
/**
* Unable to open the camera.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class CameraDeviceCallbackOpenException(val cameraId: String, val errorCode: Int) : Exception() {
override fun toString(): String {
return "CameraDeviceCallbackOpenException(cameraId='$cameraId', errorCode=$errorCode)"
}
}
/**
* Unable to configure the camera.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class CameraConfigurationFailedException(val cameraId: String) : Exception() {
override fun toString(): String {
return "CameraConfigurationFailedException(cameraId='$cameraId')"
}
}
/**
* A [CameraAdapter] that uses android's Camera 2 APIs to show previews and process images.
*/
internal class CameraAdapterImpl(
private val activity: Activity,
private val previewView: ViewGroup,
private val minimumResolution: Size,
private val cameraErrorListener: CameraErrorListener,
) : CameraAdapter>(), LifecycleObserver {
override val implementationName: String = "Camera2"
private val previewTextureView by lazy { TextureView(activity) }
private val processingImage = AtomicBoolean(false)
private val displayRotation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
activity.display?.rotation
} else {
null
} ?: @Suppress("Deprecation") activity.windowManager.defaultDisplay.rotation
private lateinit var cameraId: String
private var previewCaptureSession: CameraCaptureSession? = null
private var cameraDevice: CameraDevice? = null
private lateinit var previewSize: Size
private lateinit var previewResolution: Size
private var cameraThread: HandlerThread? = null
private var cameraHandler: Handler? = null
private var imageReader: ImageReader? = null
private var sensorRotation = 0
private val cameraOpenCloseLock = Semaphore(1)
private var flashSupported = false
private lateinit var previewRequestBuilder: CaptureRequest.Builder
private var onInitializedFlashTask: ((Boolean) -> Unit)? = null
private var autoFocusMode: Int = CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE
private val mainThreadHandler = Handler(activity.mainLooper)
private var focusPoint = PointF(previewView.width / 2F, previewView.height / 2F)
private var currentCameraIndex = -1
private lateinit var scaledPreviewSize: Rect
private val previewSurfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
openCamera()
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
configureTransform(Size(width, height), previewSize)
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean = true
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { }
}
private val imageAvailableListener = ImageReader.OnImageAvailableListener { reader ->
if (!processingImage.compareAndSet(false, true)) {
return@OnImageAvailableListener
}
reader.acquireLatestImage()?.let {
try {
it.toBitmap(getRenderScript(activity)).rotate(calculateImageRotationDegrees(displayRotation, sensorRotation).toFloat())
} catch (t: Throwable) {
Log.e(Config.logTag, "Unable to convert image to bitmap: $t")
null
} finally {
it.close()
}
}?.let {
sendImageToStream(
CameraPreviewImage(
TrackedImage(it, Stats.trackRepeatingTask("image_analysis")),
scaledPreviewSize,
),
)
}
processingImage.set(false)
}
private val stateCallback = object : CameraDevice.StateCallback() {
@Synchronized
override fun onOpened(camera: CameraDevice) {
cameraOpenCloseLock.release()
cameraDevice = camera
createCameraPreviewSession(previewResolution)
onInitializedFlashTask?.apply {
mainThreadHandler.post { this(flashSupported) }
}
}
override fun onDisconnected(camera: CameraDevice) {
cameraOpenCloseLock.release()
camera.close()
cameraDevice = null
}
override fun onError(camera: CameraDevice, error: Int) {
onDisconnected(camera)
mainThreadHandler.post {
cameraErrorListener.onCameraOpenError(CameraDeviceCallbackOpenException(camera.id, error))
}
}
}
@Synchronized
override fun withFlashSupport(task: (Boolean) -> Unit) {
if (::previewRequestBuilder.isInitialized) {
mainThreadHandler.post { task(flashSupported) }
} else {
onInitializedFlashTask = task
}
}
override fun setTorchState(on: Boolean) {
if (!::previewRequestBuilder.isInitialized) {
return
}
if (on) {
previewRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
} else {
previewRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF)
}
previewCaptureSession?.setRepeatingRequest(previewRequestBuilder.build(), null, cameraHandler)
}
override fun isTorchOn() =
if (::previewRequestBuilder.isInitialized) {
previewRequestBuilder.get(CaptureRequest.FLASH_MODE) == CaptureRequest.FLASH_MODE_TORCH
} else {
false
}
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
previewView.removeAllViews()
previewView.addView(previewTextureView)
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
startCameraThread()
if (previewTextureView.isAvailable) {
openCamera()
} else {
previewTextureView.surfaceTextureListener = previewSurfaceTextureListener
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
override fun onPause() {
super.onPause()
closeCamera()
stopCameraThread()
}
/**
* Sets up member variables related to camera.
*/
private fun setUpCameraOutputs() {
try {
getCurrentCameraDetails()?.also { cameraDetails ->
sensorRotation = cameraDetails.sensorRotation
autoFocusMode = selectAutoFocusMode(cameraDetails.supportedAutoFocusModes)
// Given a desired resolution, get a resolution and format that the camera supports.
val previewFormatAndResolution = getOptimalPreviewResolution(
getCameraResolutions(cameraDetails.config),
minimumResolution
)
// rotate the preview resolution to match the orientation
val previewFormat = previewFormatAndResolution.first
previewResolution = previewFormatAndResolution.second
previewSize = previewResolution.resolutionToSize(
displayRotation,
cameraDetails.sensorRotation,
)
Log.d(Config.logTag, "Camera2 API selected resolution $previewResolution with format $previewFormat")
imageReader = ImageReader.newInstance(previewResolution.width, previewResolution.height, previewFormat, 1)
.apply {
setOnImageAvailableListener(imageAvailableListener, cameraHandler)
}
previewTextureView.layoutParams.apply {
width = ViewGroup.LayoutParams.MATCH_PARENT
height = ViewGroup.LayoutParams.MATCH_PARENT
}
previewTextureView.requestLayout()
// Check if the flash is supported.
flashSupported = cameraDetails.flashAvailable
cameraId = cameraDetails.cameraId
}
} catch (e: CameraAccessException) {
mainThreadHandler.post { cameraErrorListener.onCameraAccessError(e) }
} catch (e: NullPointerException) {
// Currently an NPE is thrown when the Camera2API is used but not supported on the
// device this code runs.
mainThreadHandler.post { cameraErrorListener.onCameraUnsupportedError(e) }
}
}
private fun selectAutoFocusMode(supportedAutoFocusModes: List): Int =
when {
CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE in supportedAutoFocusModes ->
CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE
CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_VIDEO in supportedAutoFocusModes ->
CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_VIDEO
else -> CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE
}
private fun getCameraManager() = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
/**
* Get a list of cameraIds and their configuration maps.
*/
private val availableCameras: List by lazy {
val manager = getCameraManager()
manager.cameraIdList
.map { it to manager.getCameraCharacteristics(it) }
.mapNotNull {
it.second.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)?.run {
CameraDetails(
cameraId = it.first,
flashAvailable = it.second.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true,
config = this,
sensorRotation = it.second.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0,
supportedAutoFocusModes = it.second.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)?.toList() ?: emptyList(),
lensFacing = it.second.get(CameraCharacteristics.LENS_FACING),
)
}
}
}
private val defaultCameraIndex by lazy {
availableCameras.indexOfFirst {
it.lensFacing != CameraCharacteristics.LENS_FACING_FRONT
}
}
private fun getCurrentCameraDetails(): CameraDetails? = availableCameras.let {
if (currentCameraIndex < 0) {
currentCameraIndex = if (defaultCameraIndex >= 0) defaultCameraIndex else 0
}
if (currentCameraIndex < it.size) it[currentCameraIndex] else null
}
/**
* Get a list of all the supported camera resolutions for each format. Output is in the format:
*
* ```
* [
* (CameraFormat, Resolution),
* (CameraFormat, Resolution),
* ...
* ]
* ```
*
* Note that each format will likely have multiple resolutions. Available formats will be sorted
* by preference (fastest first).
*/
private fun getCameraResolutions(map: StreamConfigurationMap): List> {
val formats = map.outputFormats.filter { isSupportedFormat(it) }.sortedBy {
when (it) {
ImageFormat.NV21 -> 0
ImageFormat.YUV_420_888 -> 1
ImageFormat.JPEG -> 2
ImageFormat.YUY2 -> 3
else -> it
}
}
val formatToOutputSizes = formats.map { format ->
(map.getOutputSizes(format) ?: emptyArray())
.asIterable()
.map { format to it }
}
return formatToOutputSizes.flatten()
}
/**
* Opens the camera specified by [cameraId].
*/
@SuppressLint("MissingPermission")
private fun openCamera() {
setUpCameraOutputs()
previewView.apply { configureTransform(size(), previewSize) }
val manager = getCameraManager()
try {
// Wait for camera to open - 2.5 seconds is sufficient
if (!cameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
mainThreadHandler.post { cameraErrorListener.onCameraOpenError(null) }
return
}
manager.openCamera(cameraId, stateCallback, cameraHandler)
} catch (e: CameraAccessException) {
mainThreadHandler.post { cameraErrorListener.onCameraAccessError(e) }
} catch (e: InterruptedException) {
mainThreadHandler.post { cameraErrorListener.onCameraOpenError(e) }
}
}
/**
* Closes the current [CameraDevice].
*/
private fun closeCamera() {
try {
cameraOpenCloseLock.acquire()
previewCaptureSession?.close()
previewCaptureSession = null
cameraDevice?.close()
cameraDevice = null
} catch (e: InterruptedException) {
mainThreadHandler.post {
cameraErrorListener.onCameraOpenError(e)
}
} finally {
cameraOpenCloseLock.release()
}
}
/**
* Starts a background thread and its [Handler].
*/
private fun startCameraThread() {
val thread = HandlerThread("CameraBackground").also { it.start() }
cameraThread = thread
cameraHandler = Handler(thread.looper)
}
/**
* Stops the background thread and its [Handler].
*/
private fun stopCameraThread() {
cameraThread?.quitSafely()
try {
cameraThread?.join()
cameraThread = null
cameraHandler = null
} catch (e: InterruptedException) {
mainThreadHandler.post { cameraErrorListener.onCameraOpenError(e) }
}
}
/**
* Creates a new [CameraCaptureSession] for camera preview.
*/
private fun createCameraPreviewSession(previewResolution: Size) {
try {
val previewTexture = previewTextureView.surfaceTexture
// We configure the size of default buffer to be the size of camera preview we want.
previewTexture?.setDefaultBufferSize(previewResolution.width, previewResolution.height)
// This is the output Surface we need to start preview.
val previewSurface = previewTexture?.let { Surface(it) }
val imageReaderSurface = imageReader?.surface
// We set up a CaptureRequest.Builder with the output Surface.
previewRequestBuilder = cameraDevice!!.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW
)
previewSurface?.apply { previewRequestBuilder.addTarget(this) }
imageReaderSurface?.apply { previewRequestBuilder.addTarget(this) }
// Here, we create a CameraCaptureSession for camera preview.
@Suppress("Deprecation") // SessionConfiguration is not available until API 28.
cameraDevice?.createCaptureSession(
listOfNotNull(imageReaderSurface, previewSurface),
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(cameraCaptureSession: CameraCaptureSession) {
// The camera is already closed
if (cameraDevice == null) return
// When the session is ready, we start displaying the preview.
previewCaptureSession = cameraCaptureSession
try {
// Auto focus should be continuous for camera preview. This does not
// work on samsung devices.
if (!Build.MANUFACTURER.toUpperCase(Locale.ROOT).contains("SAMSUNG")) {
previewRequestBuilder.set(
CaptureRequest.CONTROL_AF_MODE,
autoFocusMode
)
}
// Finally, we start displaying the camera preview.
val previewRequest = previewRequestBuilder.build()
previewCaptureSession?.setRepeatingRequest(previewRequest, null, cameraHandler)
} catch (e: CameraAccessException) {
// Ignore camera access errors, this occurs when the camera is closed and will fire again
// when the camera is opened.
}
}
override fun onConfigureFailed(session: CameraCaptureSession) {
mainThreadHandler.post {
cameraErrorListener.onCameraOpenError(
CameraConfigurationFailedException(
session.device.id
)
)
}
}
},
null
)
} catch (e: CameraAccessException) {
mainThreadHandler.post { cameraErrorListener.onCameraAccessError(e) }
}
}
/**
* Configures the necessary [android.graphics.Matrix] transformation to `textureView`.
* This method should be called after the camera preview size is determined in
* setUpCameraOutputs and also the size of `textureView` is fixed.
*
* @param viewSize The size of `textureView`
*/
private fun configureTransform(viewSize: Size, imageSize: Size) {
val matrix = Matrix()
val viewRect = viewSize.toRectF()
val bufferRect = imageSize.toRectF()
val rotation = -(displayRotation.rotationToDegrees()).toFloat()
val imageScale = calculatePreviewScale(
viewSize = viewSize,
imageSize = imageSize,
displayRotation = displayRotation,
sensorRotationDegrees = sensorRotation,
)
val finalScale = imageScale.scale(
max(
imageScale.width * imageSize.width / viewSize.width,
imageScale.height * imageSize.height / viewSize.height,
)
)
// TODO(awushensky): this breaks on rotation. See https://stackoverflow.com/questions/34536798/android-camera2-preview-is-rotated-90deg-while-in-landscape?rq=1
bufferRect.offset(viewRect.centerX() - bufferRect.centerX(), viewRect.centerY() - bufferRect.centerY())
matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.CENTER)
matrix.postScale(finalScale.width, finalScale.height, viewRect.centerX(), viewRect.centerY())
matrix.postRotate(rotation, viewRect.centerX(), viewRect.centerY())
scaledPreviewSize = imageSize.scaleAndCenterSurrounding(viewSize)
previewTextureView.setTransform(matrix)
}
override fun setFocus(point: PointF) {
focusPoint = point
updateFocus(point)
}
private fun updateFocus(point: PointF) {
if (!::previewRequestBuilder.isInitialized) {
return
}
previewCaptureSession?.stopRepeating()
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL)
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF)
previewCaptureSession?.capture(previewRequestBuilder.build(), null, cameraHandler)
if (isMeteringAreaAFSupported()) {
previewRequestBuilder.set(
CaptureRequest.CONTROL_AF_REGIONS,
arrayOf(
MeteringRectangle(
max(point.x.toInt() - FOCUS_TOUCH_SIZE, 0),
max(point.y.toInt() - FOCUS_TOUCH_SIZE, 0),
FOCUS_TOUCH_SIZE * 2,
FOCUS_TOUCH_SIZE * 2,
MeteringRectangle.METERING_WEIGHT_MAX - 1
)
)
)
}
previewRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO)
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START)
previewRequestBuilder.setTag("FOCUS_TAG") // we'll capture this later for resuming the preview
previewCaptureSession?.capture(
previewRequestBuilder.build(),
object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult,
) {
super.onCaptureCompleted(session, request, result)
if (request.tag == "FOCUS_TAG") {
// the focus trigger is complete -
// resume repeating (preview surface will get frames), clear AF trigger
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, null)
previewCaptureSession?.setRepeatingRequest(previewRequestBuilder.build(), null, cameraHandler)
}
}
},
cameraHandler
)
}
private fun isMeteringAreaAFSupported(): Boolean {
val manager = getCameraManager()
val cameraCharacteristics = manager.getCameraCharacteristics(cameraId)
return cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) ?: 0 >= 1
}
override fun withSupportsMultipleCameras(task: (Boolean) -> Unit) {
task(availableCameras.size > 1)
}
override fun changeCamera() {
onPause()
currentCameraIndex++
if (currentCameraIndex >= availableCameras.size) {
currentCameraIndex = 0
}
onResume()
}
override fun getCurrentCamera(): Int = currentCameraIndex
}
================================================
FILE: scan-camera2/src/main/java/com/getbouncer/scan/camera/extension/CameraDetails.kt
================================================
package com.getbouncer.scan.camera.extension
import android.hardware.camera2.params.StreamConfigurationMap
/**
* Details about a camera.
*/
internal data class CameraDetails(
val cameraId: String,
val flashAvailable: Boolean,
val config: StreamConfigurationMap,
val sensorRotation: Int,
val supportedAutoFocusModes: List,
val lensFacing: Int?,
)
================================================
FILE: scan-camera2/src/main/java/com/getbouncer/scan/camera/extension/Util.kt
================================================
package com.getbouncer.scan.camera.extension
import android.util.Size
import android.util.SizeF
import android.view.Surface
import androidx.annotation.CheckResult
import com.getbouncer.scan.camera.RotationValue
/**
* The maximum resolution width for a preview.
*/
private const val MAX_RESOLUTION_WIDTH = 1920
/**
* The maximum resolution height for a preview.
*/
private const val MAX_RESOLUTION_HEIGHT = 1080
/**
* Calculate how much an image must scale in X and Y to match a view size.
*/
@CheckResult
internal fun calculatePreviewScale(
viewSize: Size,
imageSize: Size,
@RotationValue displayRotation: Int,
sensorRotationDegrees: Int
) = if (areScreenAndSensorPerpendicular(displayRotation, sensorRotationDegrees)) {
SizeF(viewSize.height.toFloat() / imageSize.height, viewSize.width.toFloat() / imageSize.width)
} else {
SizeF(viewSize.width.toFloat() / imageSize.width, viewSize.height.toFloat() / imageSize.height)
}
/**
* Convert a resolution to a size on the screen.
*/
@CheckResult
internal fun Size.resolutionToSize(
@RotationValue displayRotation: Int,
sensorRotationDegrees: Int
) = if (areScreenAndSensorPerpendicular(displayRotation, sensorRotationDegrees)) {
Size(this.height, this.width)
} else {
this
}
/**
* Determines if the dimensions are swapped given the phone's current rotation.
*
* @param displayRotation The current rotation of the display
*
* @return true if the dimensions are swapped, false otherwise.
*/
@CheckResult
internal fun areScreenAndSensorPerpendicular(
@RotationValue displayRotation: Int,
sensorRotationDegrees: Int
) = when (displayRotation) {
Surface.ROTATION_0, Surface.ROTATION_180 -> {
sensorRotationDegrees == 90 || sensorRotationDegrees == 270
}
Surface.ROTATION_90, Surface.ROTATION_270 -> {
sensorRotationDegrees == 0 || sensorRotationDegrees == 180
}
else -> {
false
}
}
/**
* Determine how much to rotate the image from the camera given the orientation of the
* display and the orientation of the camera sensor.
*
* @param displayOrientation: The enum value of the display rotation (e.g. Surface.ROTATION_0)
* @param sensorRotationDegrees: The rotation of the sensor in degrees
*
* @return the difference in degrees.
*/
@CheckResult
internal fun calculateImageRotationDegrees(
@RotationValue displayOrientation: Int,
sensorRotationDegrees: Int
) = (
(
when (displayOrientation) {
Surface.ROTATION_0 -> sensorRotationDegrees
Surface.ROTATION_90 -> sensorRotationDegrees - 90
Surface.ROTATION_180 -> sensorRotationDegrees - 180
Surface.ROTATION_270 -> sensorRotationDegrees - 270
else -> 0
} % 360
) + 360
) % 360
/**
* Get the optimal preview resolution from a list of available formats and resolutions.
*/
@CheckResult
internal fun getOptimalPreviewResolution(
cameraSizes: Iterable>,
minimumResolution: Size
): Pair {
// Only consider camera resolutions larger than the minimum resolution, but smaller than
// the maximum resolution.
val allowedCameraSizes = cameraSizes.filter {
it.second.width <= MAX_RESOLUTION_WIDTH &&
it.second.height <= MAX_RESOLUTION_HEIGHT &&
it.second.width >= minimumResolution.width &&
it.second.height >= minimumResolution.height
}
return allowedCameraSizes.minByOrNull {
it.second.width * it.second.height
} ?: DEFAULT_IMAGE_FORMAT to Size(MAX_RESOLUTION_WIDTH, MAX_RESOLUTION_HEIGHT)
}
================================================
FILE: scan-camera2/src/test/java/com/getbouncer/scan/camera/extension/UtilTest.kt
================================================
package com.getbouncer.scan.camera.extension
import android.view.Surface
import androidx.test.filters.SmallTest
import org.junit.Test
import kotlin.test.assertEquals
class UtilTest {
@Test
@SmallTest
fun calculateImageRotationDegrees_vertical() {
assertEquals(0, calculateImageRotationDegrees(Surface.ROTATION_0, 0))
assertEquals(45, calculateImageRotationDegrees(Surface.ROTATION_0, 45))
assertEquals(315, calculateImageRotationDegrees(Surface.ROTATION_0, -45))
assertEquals(180, calculateImageRotationDegrees(Surface.ROTATION_0, 180))
assertEquals(180, calculateImageRotationDegrees(Surface.ROTATION_0, -180))
}
@Test
@SmallTest
fun calculateImageRotationDegrees_right() {
assertEquals(270, calculateImageRotationDegrees(Surface.ROTATION_90, 0))
assertEquals(315, calculateImageRotationDegrees(Surface.ROTATION_90, 45))
assertEquals(225, calculateImageRotationDegrees(Surface.ROTATION_90, -45))
assertEquals(90, calculateImageRotationDegrees(Surface.ROTATION_90, 180))
assertEquals(90, calculateImageRotationDegrees(Surface.ROTATION_90, -180))
}
@Test
@SmallTest
fun calculateImageRotationDegrees_left() {
assertEquals(90, calculateImageRotationDegrees(Surface.ROTATION_270, 0))
assertEquals(135, calculateImageRotationDegrees(Surface.ROTATION_270, 45))
assertEquals(45, calculateImageRotationDegrees(Surface.ROTATION_270, -45))
assertEquals(270, calculateImageRotationDegrees(Surface.ROTATION_270, 180))
assertEquals(270, calculateImageRotationDegrees(Surface.ROTATION_270, -180))
}
@Test
@SmallTest
fun calculateImageRotationDegrees_inverted() {
assertEquals(180, calculateImageRotationDegrees(Surface.ROTATION_180, 0))
assertEquals(225, calculateImageRotationDegrees(Surface.ROTATION_180, 45))
assertEquals(135, calculateImageRotationDegrees(Surface.ROTATION_180, -45))
assertEquals(0, calculateImageRotationDegrees(Surface.ROTATION_180, 180))
assertEquals(0, calculateImageRotationDegrees(Surface.ROTATION_180, -180))
}
}
================================================
FILE: scan-camerax/.gitignore
================================================
/build
================================================
FILE: scan-camerax/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 30
buildToolsVersion '30.0.3'
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
testOptions {
unitTests.includeAndroidResources = true
}
lintOptions {
enable "Interoperability"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":scan-framework")
implementation project(':scan-camera')
implementation "androidx.appcompat:appcompat:[1.3.0,1.3.1]"
implementation "androidx.camera:camera-camera2:[1.0.0,1.0.1]"
implementation "androidx.camera:camera-core:[1.0.0,1.0.1]"
implementation "androidx.camera:camera-lifecycle:[1.0.0,1.0.1]"
implementation "androidx.camera:camera-view:[1.0.0-alpha26,1.0.0-alpha28)"
implementation "androidx.core:core-ktx:[1.3.1,1.6.0]"
}
dependencies {
testImplementation "androidx.test:core:1.4.0"
testImplementation "androidx.test:runner:1.4.0"
testImplementation "junit:junit:4.13.2"
testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30"
}
dependencies {
androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30"
}
apply from: 'deploy.gradle'
================================================
FILE: scan-camerax/deploy.gradle
================================================
apply plugin: 'maven-publish'
apply plugin: 'org.jetbrains.dokka'
apply plugin: 'signing'
task androidSourcesJar(type: Jar) {
archiveClassifier.set('sources')
if (project.plugins.findPlugin("com.android.library")) {
// Android library
from android.sourceSets.main.java.srcDirs
from android.sourceSets.main.kotlin.srcDirs
} else {
// Pure kotlin library
from sourceSets.main.java.srcDirs
from sourceSets.main.kotlin.srcDirs
}
}
tasks.withType(dokkaHtmlPartial.getClass()).configureEach {
pluginsMapConfiguration.set(
["org.jetbrains.dokka.base.DokkaBase": """{ "separateInheritedMembers": true}"""]
)
}
task javadocJar(type: Jar, dependsOn: dokkaJavadoc) {
archiveClassifier.set('javadoc')
from dokkaJavadoc.outputDirectory
}
artifacts {
archives androidSourcesJar
archives javadocJar
}
ext["signing.keyId"] = ''
ext["signing.password"] = ''
ext["signing.secretKeyRingFile"] = ''
ext["ossrhUsername"] = ''
ext["ossrhPassword"] = ''
ext["sonatypeStagingProfileId"] = ''
ext {
libraryDescription = 'This library provides the framework for cameras'
siteUrl = 'https://getbouncer.com'
scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'
scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'
scmUrl = 'https://github.com/getbouncer/cardscan-android'
licenseName = 'bouncer-free-1'
licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'
developerId = 'getbouncer'
developerName = 'Bouncer Technologies'
developerEmail = 'bouncer-support@stripe.com'
publishGroupId = 'com.getbouncer'
publishArtifactId = 'scan-camerax'
publishVersion = version
}
group = publishGroupId
version = publishVersion
File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
Properties p = new Properties()
new FileInputStream(secretPropsFile).withCloseable { is ->
p.load(is)
}
p.each { name, value ->
ext[name] = value
}
} else {
ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')
ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
}
publishing {
publications {
release(MavenPublication) {
groupId publishGroupId
artifactId publishArtifactId
version publishVersion
// Two artifacts, the `aar` (or `jar`) and the sources
if (project.plugins.findPlugin("com.android.library")) {
artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
} else {
artifact("$buildDir/libs/${project.getName()}-${version}.jar")
}
artifact androidSourcesJar
pom {
name = publishArtifactId
description = libraryDescription
url = siteUrl
licenses {
license {
name = licenseName
url = licenseUrl
}
}
developers {
developer {
id = developerId
name = developerName
email = developerEmail
}
}
scm {
connection = scmConnection
developerConnection = scmDeveloperConnection
url = scmUrl
}
// A slightly hacky fix so that your POM will include any transitive dependencies
// that your library builds upon
withXml {
def dependenciesNode = asNode().appendNode('dependencies')
project.configurations.implementation.allDependencies.each {
if (it.group != null && it.version != null) {
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', it.group)
dependencyNode.appendNode('artifactId', it.name)
dependencyNode.appendNode('version', it.version)
}
}
}
}
}
}
// The repository to publish to, Sonatype/MavenCentral
repositories {
maven {
// This is an arbitrary name, you may also use "mavencentral" or
// any other name that's descriptive for you
name = "sonatype"
url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
credentials {
username ossrhUsername
password ossrhPassword
}
}
}
}
signing {
sign publishing.publications
}
================================================
FILE: scan-camerax/src/main/AndroidManifest.xml
================================================
================================================
FILE: scan-camerax/src/main/java/com/getbouncer/scan/camera/extension/CameraAdapterImpl.kt
================================================
package com.getbouncer.scan.camera.extension
import android.app.Activity
import android.graphics.Bitmap
import android.graphics.PointF
import android.os.Build
import android.os.Handler
import android.util.DisplayMetrics
import android.util.Log
import android.util.Size
import android.view.ViewGroup
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.DisplayOrientedMeteringPointFactory
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.core.TorchState
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import com.getbouncer.scan.camera.CameraAdapter
import com.getbouncer.scan.camera.CameraErrorListener
import com.getbouncer.scan.camera.CameraPreviewImage
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.framework.Stats
import com.getbouncer.scan.framework.TrackedImage
import com.getbouncer.scan.framework.image.getRenderScript
import com.getbouncer.scan.framework.image.rotate
import com.getbouncer.scan.framework.image.size
import com.getbouncer.scan.framework.util.aspectRatio
import com.getbouncer.scan.framework.util.centerOn
import com.getbouncer.scan.framework.util.minAspectRatioSurroundingSize
import com.getbouncer.scan.framework.util.size
import com.getbouncer.scan.framework.util.toRect
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
internal class CameraAdapterImpl(
private val activity: Activity,
private val previewView: ViewGroup,
private val minimumResolution: Size,
private val cameraErrorListener: CameraErrorListener,
) : CameraAdapter>() {
override val implementationName: String = "CameraX"
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
private val mainThreadHandler = Handler(activity.mainLooper)
private var preview: Preview? = null
private var imageAnalyzer: ImageAnalysis? = null
private var camera: Camera? = null
private val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
private lateinit var lifecycleOwner: LifecycleOwner
private val cameraListeners = mutableListOf<(Camera) -> Unit>()
/** Blocking camera operations are performed using this executor */
private lateinit var cameraExecutor: ExecutorService
private val display by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
activity.display
} else {
null
} ?: @Suppress("Deprecation") activity.windowManager.defaultDisplay
}
private val displayRotation by lazy { display.rotation }
private val displayMetrics by lazy { DisplayMetrics().also { display.getRealMetrics(it) } }
private val displaySize by lazy { Size(displayMetrics.widthPixels, displayMetrics.heightPixels) }
private val previewTextureView by lazy { PreviewView(activity) }
override fun withFlashSupport(task: (Boolean) -> Unit) {
withCamera { task(it.cameraInfo.hasFlashUnit()) }
}
override fun setTorchState(on: Boolean) {
camera?.cameraControl?.enableTorch(on)
}
override fun isTorchOn(): Boolean =
camera?.cameraInfo?.torchState?.value == TorchState.ON
override fun withSupportsMultipleCameras(task: (Boolean) -> Unit) {
withCameraProvider {
task(hasBackCamera(it) && hasFrontCamera(it))
}
}
override fun changeCamera() {
withCameraProvider {
lensFacing = when {
lensFacing == CameraSelector.LENS_FACING_BACK && hasFrontCamera(it) -> CameraSelector.LENS_FACING_FRONT
lensFacing == CameraSelector.LENS_FACING_FRONT && hasBackCamera(it) -> CameraSelector.LENS_FACING_BACK
hasBackCamera(it) -> CameraSelector.LENS_FACING_BACK
hasFrontCamera(it) -> CameraSelector.LENS_FACING_FRONT
else -> CameraSelector.LENS_FACING_BACK
}
bindCameraUseCases(it)
}
}
override fun getCurrentCamera(): Int = lensFacing
override fun setFocus(point: PointF) {
camera?.let { cam ->
val meteringPointFactory = DisplayOrientedMeteringPointFactory(
display,
cam.cameraInfo,
displaySize.width.toFloat(),
displaySize.height.toFloat(),
)
val action = FocusMeteringAction.Builder(meteringPointFactory.createPoint(point.x, point.y)).build()
cam.cameraControl.startFocusAndMetering(action)
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
// Initialize our background executor
cameraExecutor = Executors.newSingleThreadExecutor()
previewView.post {
previewView.removeAllViews()
previewView.addView(previewTextureView)
previewTextureView.layoutParams.apply {
width = ViewGroup.LayoutParams.MATCH_PARENT
height = ViewGroup.LayoutParams.MATCH_PARENT
}
previewTextureView.requestLayout()
setUpCamera()
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
withCameraProvider {
it.unbindAll()
cameraExecutor.shutdown()
}
}
override fun unbindFromLifecycle(lifecycleOwner: LifecycleOwner) {
super.unbindFromLifecycle(lifecycleOwner)
withCameraProvider { cameraProvider ->
preview?.let { preview ->
cameraProvider.unbind(preview)
}
}
}
private fun setUpCamera() {
withCameraProvider {
lensFacing = when {
hasBackCamera(it) -> CameraSelector.LENS_FACING_BACK
hasFrontCamera(it) -> CameraSelector.LENS_FACING_FRONT
else -> {
mainThreadHandler.post {
cameraErrorListener.onCameraUnsupportedError(IllegalStateException("No camera is available"))
}
CameraSelector.LENS_FACING_BACK
}
}
bindCameraUseCases(it)
}
}
@Synchronized
private fun bindCameraUseCases(cameraProvider: ProcessCameraProvider) {
// CameraSelector
val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
preview = Preview.Builder()
.setTargetRotation(displayRotation)
.setTargetResolution(minimumResolution.resolutionToSize(displaySize))
.build()
imageAnalyzer = ImageAnalysis.Builder()
.setTargetRotation(displayRotation)
.setTargetResolution(minimumResolution.resolutionToSize(displaySize))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setImageQueueDepth(1)
.build()
.also { analysis ->
analysis.setAnalyzer(
cameraExecutor
) { image ->
val bitmap = image.toBitmap(getRenderScript(activity))
.rotate(image.imageInfo.rotationDegrees.toFloat())
image.close()
sendImageToStream(
CameraPreviewImage(
TrackedImage(bitmap, Stats.trackRepeatingTask("image_analysis")),
minAspectRatioSurroundingSize(
previewView.size(),
bitmap.size().aspectRatio()
).centerOn(displaySize.toRect())
)
)
}
}
cameraProvider.unbindAll()
try {
val newCamera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalyzer)
notifyCameraListeners(newCamera)
camera = newCamera
preview?.setSurfaceProvider(previewTextureView.surfaceProvider)
} catch (t: Throwable) {
Log.e(Config.logTag, "Use case camera binding failed", t)
mainThreadHandler.post { cameraErrorListener.onCameraOpenError(t) }
}
}
private fun notifyCameraListeners(camera: Camera) {
val listenerIterator = cameraListeners.iterator()
while (listenerIterator.hasNext()) {
listenerIterator.next()(camera)
listenerIterator.remove()
}
}
@Synchronized
private fun withCamera(task: (Camera) -> T) {
val camera = this.camera
if (camera != null) {
task(camera)
} else {
cameraListeners.add { task(it) }
}
}
override fun bindToLifecycle(lifecycleOwner: LifecycleOwner) {
super.bindToLifecycle(lifecycleOwner)
this.lifecycleOwner = lifecycleOwner
}
/** Returns true if the device has an available back camera. False otherwise */
private fun hasBackCamera(cameraProvider: ProcessCameraProvider): Boolean =
cameraProvider.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA)
/** Returns true if the device has an available front camera. False otherwise */
private fun hasFrontCamera(cameraProvider: ProcessCameraProvider): Boolean =
cameraProvider.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA)
/**
* Run a task with the camera provider.
*/
private fun withCameraProvider(
executor: Executor = ContextCompat.getMainExecutor(activity),
task: (ProcessCameraProvider) -> Unit,
) {
cameraProviderFuture.addListener({ task(cameraProviderFuture.get()) }, executor)
}
}
================================================
FILE: scan-camerax/src/main/java/com/getbouncer/scan/camera/extension/Image.kt
================================================
package com.getbouncer.scan.camera.extension
import android.graphics.ImageFormat
import android.renderscript.RenderScript
import androidx.annotation.CheckResult
import androidx.camera.core.ImageProxy
import com.getbouncer.scan.framework.exception.ImageTypeNotSupportedException
import com.getbouncer.scan.framework.image.NV21Image
import com.getbouncer.scan.framework.image.yuvPlanesToNV21Fast
import com.getbouncer.scan.framework.util.mapArray
import com.getbouncer.scan.framework.util.mapToIntArray
import com.getbouncer.scan.framework.util.toByteArray
/**
* Convert an ImageProxy to a bitmap.
*/
@CheckResult
internal fun ImageProxy.toBitmap(renderScript: RenderScript) = when (format) {
ImageFormat.NV21 -> NV21Image(width, height, planes[0].buffer.toByteArray()).toBitmap(renderScript)
ImageFormat.YUV_420_888 -> NV21Image(
width,
height,
yuvPlanesToNV21Fast(
width,
height,
planes.mapArray { it.buffer },
planes.mapToIntArray { it.rowStride },
planes.mapToIntArray { it.pixelStride },
),
).toBitmap(renderScript)
else -> throw ImageTypeNotSupportedException(format)
}
================================================
FILE: scan-camerax/src/main/java/com/getbouncer/scan/camera/extension/Util.kt
================================================
package com.getbouncer.scan.camera.extension
import android.util.Size
import kotlin.math.max
import kotlin.math.min
/**
* Convert a resolution to a size on the screen based only on the display size.
*/
internal fun Size.resolutionToSize(displaySize: Size) = when {
displaySize.width >= displaySize.height -> Size(
/* width */
max(width, height),
/* height */
min(width, height),
)
else -> Size(
/* width */
min(width, height),
/* height */
max(width, height),
)
}
================================================
FILE: scan-framework/.gitignore
================================================
/build
================================================
FILE: scan-framework/README.md
================================================
# Deprecation Notice
Hello from the Stripe (formerly Bouncer) team!
We'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.
This 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!
If 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.
If 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).
For the new product, please visit the [stripe github repository](https://github.com/stripe/stripe-android/tree/master/stripecardscan).
# Scan Framework
This 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.
Note 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.
Scan Framework serves as the foundation for CardScan and CardVerify enterprise libraries, which validate the authenticity of payment cards as they are scanned.

## Contents
* [Requirements](#requirements)
* [Demo](#demo)
* [Integration](#integration)
* [Using](#using)
* [Developing](#developing)
* [Authors](#authors)
* [License](#license)
## Requirements
* Android API level 21 or higher
* Kotlin coroutine compatibility
Note: Your app does not have to be written in kotlin to integrate this library, but must be able to depend on kotlin functionality.
## Demo
An app demonstrating the basic capabilities of CardScan is available in [github](https://github.com/getbouncer/cardscan-demo-android).
## Integration
See the [integration documentation](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) in the Bouncer Docs.
## Using
This 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.
For 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).
### Processing unlimited data
Let's use an example where we process an unknown number of `MyData` values into `MyAnalyzerOutput` values, and then aggregate them into a single `MyAnalyzerOutput`.
First, create our input and output data types:
```kotlin
data class MyData(data: String)
data class MyAnalyzerOutput(output: Int)
```
Next, create an analyzer to process inputs into outputs, and a factory to create new instances of the analyzer.
```kotlin
class MyAnalyzer : Analyzer {
override suspend fun analyze(data: MyData, state: Unit): MyAnalyzerOutput = MyAnalyzerOutput(data.data.length)
override val name = "my_analyzer"
}
class MyAnalyzerFactory : AnalyzerFactory {
override suspend fun newInstance(): Analyzer? = MyAnalyzer()
}
```
Then, create a result handler to aggregate multiple outputs into one, and indicate when processing should cease.
```kotlin
class MyResultHandler(listener: ResultHanlder) :
StateUpdatingResultHandler, MyAnalyzerOutput>() {
private var resultsReceived = 0
private var totalResult = 0
override suspend fun onResult(
result: MyAnalyzerOutput,
state: LoopState,
data: MyData,
updateState: (LoopState) -> Unit
) {
resultsReceived++
if (resultsReceived > 10) {
updateState(state.copy(finished = true))
listener.onResult(MyAnalyzerOutput(totalResult), state.state, data)
} else {
totalResult += result.output
}
}
}
```
Finally, tie it all together with a class that receives data and does something with the result.
```kotlin
class MyDataProcessor : CoroutineScope, ResultHandler {
private val analyzerPool = AnalyzerPool.Factory(MyAnalyzerFactory(), 4)
private val resultHandler = MyResultHandler(this)
private val loop by lazy {
ProcessBoundAnalyzerLoop(analyzerPool, resultHandler, Unit, "my_loop", { true }, { true })
}
fun subscribeTo(flow: Flow) {
loop.subscribeTo(flow, this)
}
fun onResult(result: MyAnalyzerOutput, state: Unit, data: MyData) {
// Display something
}
}
```
### Processing a known amount of data
In this example, we need to process a known amount of data as quickly as possible using multiple analyzers.
First, create our input and output data types:
```kotlin
data class MyData(data: String)
data class MyAnalyzerOutput(output: Int)
```
Next, create an analyzer to process inputs into outputs, and a factory to create new instances of the analyzer.
```kotlin
class MyAnalyzer : Analyzer {
override suspend fun analyze(data: MyData, state: Unit): MyAnalyzerOutput = data.data.length
}
class MyAnalyzerFactory : AnalyzerFactory {
override fun newInstance(): Analyzer? = MyAnalyzer()
}
```
Finally, tie it all together with a class that processes the data and does something with the results.
```kotlin
class MyDataProcessor : CoroutineScope, TerminatingResultHandler {
override val coroutineContext: CoroutineContext = Dispatchers.Default
private val analyzerFactory = MyAnalyzerFactory()
private val resultHandler = MyResultHandler(this)
private val analyzerPool = AnalyzerPool(analyzerFactory)
private val loop: AnalyzerLoop by lazy {
FiniteAnalyzerLoop(
analyzerPool = analyzerPool,
resultHandler = this,
initialState = Unit,
name = "loop_name",
onAnalyzerFailure = {
launch(Dispatchers.Main) { analyzerFailure(it) }
true // terminate the loop on any analyzer failures
},
onResultFailure = {
launch(Dispatchers.Main) { analyzerFailure(it) }
true // terminate the loop on any result handler failures
},
timeLimit = 10.seconds
)
}
fun processData(data: List) {
loop.process(data, this)
}
override fun onResult(result: MyAnalyzerOutput, state: Unit, data: MyData) {
// A single frame has been processed
}
override fun onAllDataProcessed() {
// Notify that all data has been processed
}
override fun onTerminatedEarly() {
// Notify that not all data was processed
}
private fun analyzerFailure(cause: Throwable?) {
// Notify that the data processing failed
}
}
```
## Developing
See the [development docs](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) for details on developing this library.
## Authors
Adam Wushensky, Sam King, and Zain ul Abi Din
## License
This library is available under the MIT license. See the [LICENSE](../LICENSE) file for the full license text.
================================================
FILE: scan-framework/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
android {
compileSdkVersion 30
buildToolsVersion '30.0.3'
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
buildConfigField("String", "SDK_VERSION_STRING", "\"${version}\"")
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
testOptions {
unitTests.includeAndroidResources = true
}
lintOptions {
enable "Interoperability"
}
packagingOptions {
pickFirst 'META-INF/AL2.0'
pickFirst 'META-INF/LGPL2.1'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
aaptOptions {
noCompress "tflite"
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "androidx.core:core-ktx:[1.3.1,1.6.0]"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:[1.4.0,1.5.1]"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:[1.4.0,1.5.1]"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:[1.1.0,1.2.2]"
// Allow the user to specify their own version of Tensorflow Lite to include
runtimeOnly project(":tensorflow-lite")
compileOnly "org.tensorflow:tensorflow-lite:2.4.0"
}
dependencies {
testImplementation "androidx.test:core:1.4.0"
testImplementation "androidx.test:runner:1.4.0"
testImplementation "junit:junit:4.13.2"
testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1"
}
dependencies {
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1"
}
apply from: "deploy.gradle"
================================================
FILE: scan-framework/consumer-rules.pro
================================================
================================================
FILE: scan-framework/deploy.gradle
================================================
apply plugin: 'maven-publish'
apply plugin: 'org.jetbrains.dokka'
apply plugin: 'signing'
task androidSourcesJar(type: Jar) {
archiveClassifier.set('sources')
if (project.plugins.findPlugin("com.android.library")) {
// Android library
from android.sourceSets.main.java.srcDirs
from android.sourceSets.main.kotlin.srcDirs
} else {
// Pure kotlin library
from sourceSets.main.java.srcDirs
from sourceSets.main.kotlin.srcDirs
}
}
tasks.withType(dokkaHtmlPartial.getClass()).configureEach {
pluginsMapConfiguration.set(
["org.jetbrains.dokka.base.DokkaBase": """{ "separateInheritedMembers": true}"""]
)
}
task javadocJar(type: Jar, dependsOn: dokkaJavadoc) {
archiveClassifier.set('javadoc')
from dokkaJavadoc.outputDirectory
}
artifacts {
archives androidSourcesJar
archives javadocJar
}
ext["signing.keyId"] = ''
ext["signing.password"] = ''
ext["signing.secretKeyRingFile"] = ''
ext["ossrhUsername"] = ''
ext["ossrhPassword"] = ''
ext["sonatypeStagingProfileId"] = ''
ext {
libraryDescription = 'This library provides the framework for scanning'
siteUrl = 'https://getbouncer.com'
scmConnection = 'scm:git:github.com/getbouncer/cardscan-android.git'
scmDeveloperConnection = 'scm:git:https://github.com/getbouncer/cardscan-android.git'
scmUrl = 'https://github.com/getbouncer/cardscan-android'
licenseName = 'bouncer-free-1'
licenseUrl = 'https://github.com/getbouncer/cardscan-android/blob/master/LICENSE'
developerId = 'getbouncer'
developerName = 'Bouncer Technologies'
developerEmail = 'bouncer-support@stripe.com'
publishGroupId = 'com.getbouncer'
publishArtifactId = 'scan-framework'
publishVersion = version
}
group = publishGroupId
version = publishVersion
File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
Properties p = new Properties()
new FileInputStream(secretPropsFile).withCloseable { is ->
p.load(is)
}
p.each { name, value ->
ext[name] = value
}
} else {
ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')
ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
}
publishing {
publications {
release(MavenPublication) {
groupId publishGroupId
artifactId publishArtifactId
version publishVersion
// Two artifacts, the `aar` (or `jar`) and the sources
if (project.plugins.findPlugin("com.android.library")) {
artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
} else {
artifact("$buildDir/libs/${project.getName()}-${version}.jar")
}
artifact androidSourcesJar
pom {
name = publishArtifactId
description = libraryDescription
url = siteUrl
licenses {
license {
name = licenseName
url = licenseUrl
}
}
developers {
developer {
id = developerId
name = developerName
email = developerEmail
}
}
scm {
connection = scmConnection
developerConnection = scmDeveloperConnection
url = scmUrl
}
// A slightly hacky fix so that your POM will include any transitive dependencies
// that your library builds upon
withXml {
def dependenciesNode = asNode().appendNode('dependencies')
project.configurations.implementation.allDependencies.each {
if (it.group != null && it.version != null) {
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', it.group)
dependencyNode.appendNode('artifactId', it.name)
dependencyNode.appendNode('version', it.version)
}
}
}
}
}
}
// The repository to publish to, Sonatype/MavenCentral
repositories {
maven {
// This is an arbitrary name, you may also use "mavencentral" or
// any other name that's descriptive for you
name = "sonatype"
url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
credentials {
username ossrhUsername
password ossrhPassword
}
}
}
}
signing {
sign publishing.publications
}
================================================
FILE: scan-framework/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class org.tensorflow.lite.Interpreter { *; }
================================================
FILE: scan-framework/src/androidTest/assets/sample_resource.tflite
================================================
ABC123
DEF456
================================================
FILE: scan-framework/src/androidTest/java/com/getbouncer/scan/framework/FetcherTest.kt
================================================
package com.getbouncer.scan.framework
import androidx.test.filters.LargeTest
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.net.URL
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
class FetcherTest {
private val testContext = InstrumentationRegistry.getInstrumentation().context
@Before
fun before() {
Config.apiKey = "qOJ_fF-WLDMbG05iBq5wvwiTNTmM2qIn"
}
@After
fun after() {
Config.apiKey = null
}
@Test
@SmallTest
@ExperimentalCoroutinesApi
fun fetchResource_success() = runBlockingTest {
class ResourceFetcherImpl : ResourceFetcher() {
override val assetFileName: String = "sample_resource.tflite"
override val modelVersion: String = "sample_resource"
override val hash: String = "0dcf3e387c68dfea8dd72a183f1f765478ebaa4d8544cfc09a16e87a795d8ccf"
override val hashAlgorithm: String = "SHA-256"
override val modelClass: String = "sample_class"
override val modelFrameworkVersion: Int = 2049
}
assertEquals(
expected = FetchedResource(
modelClass = "sample_class",
modelFrameworkVersion = 2049,
modelVersion = "sample_resource",
modelHash = "0dcf3e387c68dfea8dd72a183f1f765478ebaa4d8544cfc09a16e87a795d8ccf",
modelHashAlgorithm = "SHA-256",
assetFileName = "sample_resource.tflite",
),
actual = ResourceFetcherImpl().fetchData(forImmediateUse = false, isOptional = false)
)
}
@Test
@LargeTest
fun fetchModelFromWebDirectly_success() = runBlocking {
class FetcherImpl : DirectDownloadWebFetcher(testContext) {
override val url = URL("https://downloads.getbouncer.com/ocr/darknite/android/darknite.tflite")
override val hash = "0ef6e590a5c8b0da63546079a0afacd8ccb72418af68972b72fda45deaca543a"
override val hashAlgorithm = "SHA-256"
override val modelVersion = "darknite"
override val modelClass = "ocr"
override val modelFrameworkVersion = 1
}
// force downloading the model for this test
val fetcher = FetcherImpl()
fetcher.clearCache()
val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)
assertTrue { fetchedModel is FetchedFile }
val file = (fetchedModel as FetchedFile).file
assertNotNull(file)
val reader = file.reader()
reader.skip(4)
assertEquals('T', reader.read().toChar())
assertEquals('F', reader.read().toChar())
assertEquals('L', reader.read().toChar())
assertEquals('3', reader.read().toChar())
}
@Test
@LargeTest
fun fetchModelFromWebSignedUrl_success() = runBlocking {
class FetcherImpl : SignedUrlModelWebFetcher(testContext) {
override val modelClass = "four_recognize"
override val modelFrameworkVersion = 1
override val modelVersion = "0.0.1.16"
override val modelFileName = "fourrecognize.tflite"
override val hash = "55eea0d57239a7e92904fb15209963f7236bd06919275bdeb0a765a94b559c97"
override val hashAlgorithm = "SHA-256"
}
// force downloading the model for this test
val fetcher = FetcherImpl()
fetcher.clearCache()
val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)
assertTrue { fetchedModel is FetchedFile }
val file = (fetchedModel as FetchedFile).file
assertNotNull(file)
val reader = file.reader()
reader.skip(4)
assertEquals('T', reader.read().toChar())
assertEquals('F', reader.read().toChar())
assertEquals('L', reader.read().toChar())
assertEquals('3', reader.read().toChar())
}
@Test
@LargeTest
fun fetchModelFromWebSignedUrl_downloadFail() = runBlocking {
class FetcherImpl : SignedUrlModelWebFetcher(testContext) {
override val modelClass = "invalid_model"
override val modelFrameworkVersion = 1
override val modelVersion = "0.0.1.16"
override val modelFileName = "fourrecognize.tflite"
override val hash = "55eea0d57239a7e92904fb15209963f7236bd06919275bdeb0a765a94b559c97"
override val hashAlgorithm = "SHA-256"
}
// force downloading the model for this test
val fetcher = FetcherImpl()
fetcher.clearCache()
val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)
assertTrue { fetchedModel is FetchedFile }
assertNull((fetchedModel as FetchedFile).file)
}
@Test
@LargeTest
fun fetchUpgradableModelFromWeb_success() = runBlocking {
class FetcherImpl : UpdatingModelWebFetcher(testContext) {
override val modelClass = "four_recognize"
override val modelFrameworkVersion = 1
override val defaultModelVersion = "0.0.1.16"
override val defaultModelFileName = "fourrecognize.tflite"
override val defaultModelHash = "abc"
override val defaultModelHashAlgorithm = "SHA-256"
}
// force downloading the model for this test
val fetcher = FetcherImpl()
fetcher.clearCache()
val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)
assertTrue { fetchedModel is FetchedFile }
val file = (fetchedModel as FetchedFile).file
assertNotNull(file)
val reader = file.reader()
reader.skip(4)
assertEquals('T', reader.read().toChar())
assertEquals('F', reader.read().toChar())
assertEquals('L', reader.read().toChar())
assertEquals('3', reader.read().toChar())
}
@Test
@LargeTest
fun fetchUpgradableModelFromWeb_fallbackSuccess() = runBlocking {
class FetcherImpl : UpdatingModelWebFetcher(testContext) {
override val modelClass = "four_recognize"
override val modelFrameworkVersion = 2049
override val defaultModelVersion = "0.0.1.16"
override val defaultModelFileName = "fourrecognize.tflite"
override val defaultModelHash = "55eea0d57239a7e92904fb15209963f7236bd06919275bdeb0a765a94b559c97"
override val defaultModelHashAlgorithm = "SHA-256"
}
// force downloading the model for this test
val fetcher = FetcherImpl()
fetcher.clearCache()
val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)
assertTrue { fetchedModel is FetchedFile }
val file = (fetchedModel as FetchedFile).file
assertNotNull(file)
val reader = file.reader()
reader.skip(4)
assertEquals('T', reader.read().toChar())
assertEquals('F', reader.read().toChar())
assertEquals('L', reader.read().toChar())
assertEquals('3', reader.read().toChar())
}
@Test
@LargeTest
fun fetchUpgradableModelFromWeb_successForImmediateUse() = runBlocking {
class FetcherImpl : UpdatingModelWebFetcher(testContext) {
override val modelClass = "four_recognize"
override val modelFrameworkVersion = 2049
override val defaultModelVersion = "0.0.1.16"
override val defaultModelFileName = "fourrecognize.tflite"
override val defaultModelHash = "55eea0d57239a7e92904fb15209963f7236bd06919275bdeb0a765a94b559c97"
override val defaultModelHashAlgorithm = "SHA-256"
}
// force downloading the model for this test
val fetcher = FetcherImpl()
fetcher.clearCache()
val fetchedModel = fetcher.fetchData(forImmediateUse = true, isOptional = false)
assertTrue { fetchedModel is FetchedFile }
val file = (fetchedModel as FetchedFile).file
assertNotNull(file)
val reader = file.reader()
reader.skip(4)
assertEquals('T', reader.read().toChar())
assertEquals('F', reader.read().toChar())
assertEquals('L', reader.read().toChar())
assertEquals('3', reader.read().toChar())
}
@Test
@LargeTest
fun fetchUpgradableModelFromWeb_fail() = runBlocking {
class FetcherImpl : UpdatingModelWebFetcher(testContext) {
override val modelClass = "four_recognize"
override val modelFrameworkVersion = 2049
override val defaultModelVersion = "0.0.1.16"
override val defaultModelFileName = "fourrecognize.tflite"
override val defaultModelHash = "abc"
override val defaultModelHashAlgorithm = "SHA-256"
}
// force downloading the model for this test
val fetcher = FetcherImpl()
fetcher.clearCache()
val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)
assertTrue { fetchedModel is FetchedFile }
assertNull((fetchedModel as FetchedFile).file)
}
@Test
@LargeTest
fun fetchUpgradableResourceModel_success() = runBlocking {
class FetcherImpl : UpdatingResourceFetcher(testContext) {
override val assetFileName: String = "sample_resource.tflite"
override val resourceModelVersion = "demo"
override val resourceModelHash = "0dcf3e387c68dfea8dd72a183f1f765478ebaa4d8544cfc09a16e87a795d8ccf"
override val resourceModelHashAlgorithm = "SHA-256"
override val modelClass = "four_recognize"
override val modelFrameworkVersion = 1
}
// force downloading the model for this test
val fetcher = FetcherImpl()
fetcher.clearCache()
val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)
assertTrue("fetchedModel is $fetchedModel") { fetchedModel is FetchedFile }
val file = (fetchedModel as FetchedFile).file
assertNotNull(file)
val reader = file.reader()
reader.skip(4)
assertEquals('T', reader.read().toChar())
assertEquals('F', reader.read().toChar())
assertEquals('L', reader.read().toChar())
assertEquals('3', reader.read().toChar())
}
@Test
@LargeTest
fun fetchUpgradableResourceModel_successForImmediateUse() = runBlocking {
class FetcherImpl : UpdatingResourceFetcher(testContext) {
override val assetFileName: String = "sample_resource.tflite"
override val resourceModelVersion = "demo"
override val resourceModelHash = "0dcf3e387c68dfea8dd72a183f1f765478ebaa4d8544cfc09a16e87a795d8ccf"
override val resourceModelHashAlgorithm = "SHA-256"
override val modelClass = "four_recognize"
override val modelFrameworkVersion = 1
}
// force downloading the model for this test
val fetcher = FetcherImpl()
fetcher.clearCache()
val fetchedModel = fetcher.fetchData(forImmediateUse = true, isOptional = false)
assertTrue { fetchedModel is FetchedResource }
assertEquals("sample_resource.tflite", (fetchedModel as FetchedResource).assetFileName)
}
@Test
@LargeTest
fun fetchUpgradableResourceModel_downloadFail() = runBlocking {
class FetcherImpl : UpdatingResourceFetcher(testContext) {
override val assetFileName: String = "sample_resource.tflite"
override val resourceModelVersion = "demo"
override val resourceModelHash = "0dcf3e387c68dfea8dd72a183f1f765478ebaa4d8544cfc09a16e87a795d8ccf"
override val resourceModelHashAlgorithm = "SHA-256"
override val modelClass = "invalid_model_class"
override val modelFrameworkVersion = 1
}
// force downloading the model for this test
val fetcher = FetcherImpl()
fetcher.clearCache()
val fetchedModel = fetcher.fetchData(forImmediateUse = false, isOptional = false)
assertTrue { fetchedModel is FetchedResource }
assertEquals("sample_resource.tflite", (fetchedModel as FetchedResource).assetFileName)
}
}
================================================
FILE: scan-framework/src/androidTest/java/com/getbouncer/scan/framework/LoaderTest.kt
================================================
package com.getbouncer.scan.framework
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
import org.junit.Test
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class LoaderTest {
private val testContext = InstrumentationRegistry.getInstrumentation().context
@Test
@SmallTest
fun loadData_fromResource_success() = runBlocking {
val fetchedData = FetchedResource(
modelClass = "sample_class",
modelFrameworkVersion = 2049,
modelVersion = "sample_resource",
modelHash = "0dcf3e387c68dfea8dd72a183f1f765478ebaa4d8544cfc09a16e87a795d8ccf",
modelHashAlgorithm = "SHA-256",
assetFileName = "sample_resource.tflite",
)
val byteBuffer = Loader(testContext).loadData(fetchedData)
assertNotNull(byteBuffer)
assertEquals(14, byteBuffer.limit(), "File is not expected size")
byteBuffer.rewind()
// ensure not all bytes are zero
var encounteredNonZeroByte = false
while (!encounteredNonZeroByte) {
encounteredNonZeroByte = byteBuffer.get().toInt() != 0
}
assertTrue(encounteredNonZeroByte, "All bytes were zero")
// ensure bytes are correct
byteBuffer.rewind()
assertEquals('A', byteBuffer.get().toInt().toChar())
assertEquals('B', byteBuffer.get().toInt().toChar())
assertEquals('C', byteBuffer.get().toInt().toChar())
assertEquals('1', byteBuffer.get().toInt().toChar())
}
@Test
@SmallTest
fun loadData_fromFile_success() = runBlocking {
val sampleFile = File(testContext.cacheDir, "sample_file")
if (sampleFile.exists()) {
sampleFile.delete()
}
sampleFile.createNewFile()
sampleFile.writeText("ABC123")
val fetchedData = FetchedFile(
modelClass = "sample_class",
modelFrameworkVersion = 2049,
modelVersion = "sample_file",
modelHash = "133351546614bfadfa68bb66c22a06265972b02791e4ac545ad900f20fe1a796",
modelHashAlgorithm = "SHA-256",
file = sampleFile,
)
val byteBuffer = Loader(testContext).loadData(fetchedData)
assertNotNull(byteBuffer)
assertEquals(6, byteBuffer.limit(), "File is not expected size")
byteBuffer.rewind()
// ensure not all bytes are zero
var encounteredNonZeroByte = false
while (!encounteredNonZeroByte) {
encounteredNonZeroByte = byteBuffer.get().toInt() != 0
}
assertTrue(encounteredNonZeroByte, "All bytes were zero")
// ensure bytes are correct
byteBuffer.rewind()
assertEquals('A', byteBuffer.get().toInt().toChar())
assertEquals('B', byteBuffer.get().toInt().toChar())
assertEquals('C', byteBuffer.get().toInt().toChar())
assertEquals('1', byteBuffer.get().toInt().toChar())
}
}
================================================
FILE: scan-framework/src/androidTest/java/com/getbouncer/scan/framework/StorageTest.kt
================================================
package com.getbouncer.scan.framework
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
private const val PURPOSE_TEST = "test"
class StorageTest {
private val testContext = InstrumentationRegistry.getInstrumentation().context
@Test
@SmallTest
fun storeAndRetrieveString() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertTrue(storage.storeValue(key, "test_string"))
assertEquals("test_string", storage.getString(key, "wrong"))
}
@Test
@SmallTest
fun storeAndRetrieveLong() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertTrue(storage.storeValue(key, 1L))
assertEquals(1L, storage.getLong(key, 2L))
}
@Test
@SmallTest
fun storeAndRetrieveInt() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertTrue(storage.storeValue(key, 1))
assertEquals(1, storage.getInt(key, 2))
}
@Test
@SmallTest
fun storeAndRetrieveFloat() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertTrue(storage.storeValue(key, 1F))
assertEquals(1F, storage.getFloat(key, 2F))
}
@Test
@SmallTest
fun storeAndRetrieveBoolean() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertTrue(storage.storeValue(key, true))
assertEquals(true, storage.getBoolean(key, false))
}
@Test
@SmallTest
fun retrieveMissingString() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertEquals("default", storage.getString(key, "default"))
}
@Test
@SmallTest
fun retrieveMissingLong() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertEquals(1L, storage.getLong(key, 1L))
}
@Test
@SmallTest
fun retrieveMissingInt() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertEquals(1, storage.getInt(key, 1))
}
@Test
@SmallTest
fun retrieveMissingFloat() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertEquals(1F, storage.getFloat(key, 1F))
}
@Test
@SmallTest
fun retrieveMissingBoolean() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertEquals(true, storage.getBoolean(key, true))
}
@Test
@SmallTest
fun retrieveWrongTypeString() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertTrue(storage.storeValue(key, 1L))
assertEquals("default", storage.getString(key, "default"))
}
@Test
@SmallTest
fun retrieveWrongTypeLong() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertTrue(storage.storeValue(key, 1F))
assertEquals(1L, storage.getLong(key, 1L))
}
@Test
@SmallTest
fun retrieveWrongTypeInt() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertTrue(storage.storeValue(key, 1F))
assertEquals(1, storage.getInt(key, 1))
}
@Test
@SmallTest
fun retrieveWrongTypeFloat() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertTrue(storage.storeValue(key, true))
assertEquals(1F, storage.getFloat(key, 1F))
}
@Test
@SmallTest
fun retrieveWrongTypeBoolean() {
val storage = StorageFactory.getStorageInstance(testContext, PURPOSE_TEST)
val key = "test"
assertTrue(storage.storeValue(key, "test_value"))
assertEquals(true, storage.getBoolean(key, true))
}
}
================================================
FILE: scan-framework/src/androidTest/java/com/getbouncer/scan/framework/api/BouncerApiTest.kt
================================================
package com.getbouncer.scan.framework.api
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.framework.Stats
import com.getbouncer.scan.framework.api.dto.AppInfo
import com.getbouncer.scan.framework.api.dto.BouncerErrorResponse
import com.getbouncer.scan.framework.api.dto.ClientDevice
import com.getbouncer.scan.framework.api.dto.ModelVersion
import com.getbouncer.scan.framework.api.dto.ScanStatistics
import com.getbouncer.scan.framework.api.dto.StatsPayload
import com.getbouncer.scan.framework.ml.getLoadedModelVersions
import com.getbouncer.scan.framework.ml.trackModelLoaded
import com.getbouncer.scan.framework.util.AppDetails
import com.getbouncer.scan.framework.util.Device
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.serialization.Serializable
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.fail
class BouncerApiTest {
companion object {
private const val STATS_PATH = "/scan_stats"
}
private val testContext = InstrumentationRegistry.getInstrumentation().context
private val appContext = InstrumentationRegistry.getInstrumentation().targetContext
@Before
fun before() {
Config.apiKey = "qOJ_fF-WLDMbG05iBq5wvwiTNTmM2qIn"
}
@After
fun after() {
Config.apiKey = null
}
@Test
@LargeTest
@ExperimentalCoroutinesApi
fun uploadScanStats_success() = runBlockingTest {
for (i in 0..100) {
Stats.trackRepeatingTask("test_repeating_task_1").trackResult("$i")
}
for (i in 0..100) {
Stats.trackRepeatingTask("test_repeating_task_2").trackResult("$i")
}
val task1 = Stats.trackTask("test_task_1")
for (i in 0..5) {
task1.trackResult("$i")
}
trackModelLoaded("test_model_class", "test_model_vesion", 2, true)
when (
val result = postForResult(
context = appContext,
path = STATS_PATH,
data = StatsPayload(
instanceId = "test_instance_id",
scanId = "test_scan_id",
device = ClientDevice.fromDevice(Device.fromContext(testContext)),
app = AppInfo.fromAppDetails(AppDetails.fromContext(testContext)),
scanStats = ScanStatistics.fromStats(),
modelVersions = getLoadedModelVersions().map { ModelVersion.fromModelLoadDetails(it) },
),
requestSerializer = StatsPayload.serializer(),
responseSerializer = ScanStatsResults.serializer(),
errorSerializer = BouncerErrorResponse.serializer()
)
) {
is NetworkResult.Success -> {
assertEquals(200, result.responseCode)
}
else -> fail("Network result was not success: $result")
}
}
/**
* TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with
* runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or
* withContext(Dispatchers.Default).
*
* See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details.
*/
@Test
@LargeTest
fun getModelSignedUrl() = runBlocking {
when (
val result = getModelSignedUrl(
appContext,
"four_recognize",
"v0.0.1",
"model.tflite"
)
) {
is NetworkResult.Success -> {
assertNotNull(result.body.modelUrl)
assertNotEquals("", result.body.modelUrl)
}
else -> fail("network result was not success: $result")
}
}
@Serializable
data class ScanStatsResults(val status: String? = "")
}
================================================
FILE: scan-framework/src/androidTest/java/com/getbouncer/scan/framework/image/BitmapExtensionsTest.kt
================================================
package com.getbouncer.scan.framework.image
import android.graphics.Rect
import android.util.Size
import androidx.core.graphics.drawable.toBitmap
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.getbouncer.scan.framework.test.R
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class BitmapExtensionsTest {
private val testResources = InstrumentationRegistry.getInstrumentation().context.resources
@Test
@SmallTest
fun bitmap_scale_isCorrect() {
// read in a sample bitmap file
val bitmap = testResources.getDrawable(R.drawable.ocr_card_numbers_clear, null).toBitmap()
assertNotNull(bitmap)
assertEquals(600, bitmap.width, "Bitmap width is not expected")
assertEquals(375, bitmap.height, "Bitmap height is not expected")
// scale the bitmap
val scaledBitmap = bitmap.scale(0.2F)
// check the expected sizes of the images
assertEquals(
Size(bitmap.width / 5, bitmap.height / 5),
Size(scaledBitmap.width, scaledBitmap.height),
"Scaled image is the wrong size"
)
// check each pixel of the images
var encounteredNonZeroPixel = false
for (x in 0 until scaledBitmap.width) {
for (y in 0 until scaledBitmap.height) {
encounteredNonZeroPixel = encounteredNonZeroPixel || scaledBitmap.getPixel(x, y) != 0
}
}
assertTrue(encounteredNonZeroPixel, "Pixels were all zero")
}
@Test
@SmallTest
fun bitmap_crop_isCorrect() {
val bitmap = testResources.getDrawable(R.drawable.ocr_card_numbers_clear, null).toBitmap()
assertNotNull(bitmap)
assertEquals(600, bitmap.width, "Bitmap width is not expected")
assertEquals(375, bitmap.height, "Bitmap height is not expected")
// crop the bitmap
val croppedBitmap = bitmap.crop(
Rect(
bitmap.width / 4,
bitmap.height / 4,
bitmap.width * 3 / 4,
bitmap.height * 3 / 4
)
)
// check the expected sizes of the images
assertEquals(
Size(bitmap.width * 3 / 4 - bitmap.width / 4, bitmap.height * 3 / 4 - bitmap.height / 4),
Size(croppedBitmap.width, croppedBitmap.height),
"Cropped image is the wrong size"
)
// check each pixel of the images
var encounteredNonZeroPixel = false
for (x in 0 until croppedBitmap.width) {
for (y in 0 until croppedBitmap.height) {
val croppedPixel = croppedBitmap.getPixel(x, y)
val originalPixel = bitmap.getPixel(x + bitmap.width / 4, y + bitmap.height / 4)
assertEquals(originalPixel, croppedPixel, "Difference at pixel $x, $y")
encounteredNonZeroPixel = encounteredNonZeroPixel || croppedPixel != 0
}
}
assertTrue(encounteredNonZeroPixel, "Pixels were all zero")
}
}
================================================
FILE: scan-framework/src/androidTest/java/com/getbouncer/scan/framework/layout/LayoutTest.kt
================================================
package com.getbouncer.scan.framework.layout
import android.graphics.Rect
import android.util.Size
import androidx.test.filters.SmallTest
import com.getbouncer.scan.framework.util.adjustSizeToAspectRatio
import com.getbouncer.scan.framework.util.centerScaled
import com.getbouncer.scan.framework.util.intersectionWith
import com.getbouncer.scan.framework.util.maxAspectRatioInSize
import com.getbouncer.scan.framework.util.minAspectRatioSurroundingSize
import com.getbouncer.scan.framework.util.move
import com.getbouncer.scan.framework.util.projectRegionOfInterest
import com.getbouncer.scan.framework.util.scaleAndCenterWithin
import org.junit.Test
import java.lang.IllegalArgumentException
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class LayoutTest {
@Test
@SmallTest
fun maxAspectRatioInSize_sameRatio() {
// the same aspect ratio as the size
assertEquals(Size(16, 9), maxAspectRatioInSize(Size(16, 9), 16.toFloat() / 9))
}
@Test
@SmallTest
fun maxAspectRatioInSize_wide() {
// an aspect ratio that's wider than tall
assertEquals(Size(16, 9), maxAspectRatioInSize(Size(16, 16), 16.toFloat() / 9))
}
@Test
@SmallTest
fun maxAspectRatioInSize_tall() {
// an aspect ratio that's taller than wide
assertEquals(Size(9, 16), maxAspectRatioInSize(Size(16, 16), 9.toFloat() / 16))
}
@Test
@SmallTest
fun scaleAndCenterWithin_horizontal() {
// center horizontally
assertEquals(Rect(5, 0, 20, 15), Size(4, 4).scaleAndCenterWithin(Size(25, 15)))
}
@Test
@SmallTest
fun scaleAndCenterWithin_vertical() {
// center vertically
assertEquals(Rect(0, 5, 15, 20), Size(4, 4).scaleAndCenterWithin(Size(15, 25)))
}
@Test
@SmallTest
fun scaleAndCenterWithin_sameSquare() {
// same ratio
assertEquals(Rect(0, 0, 15, 15), Size(4, 4).scaleAndCenterWithin(Size(15, 15)))
}
@Test
@SmallTest
fun scaleAndCenterWithin_sameRectangle() {
// same ratio, not square
assertEquals(Rect(0, 0, 25, 15), Size(5, 3).scaleAndCenterWithin(Size(25, 15)))
}
@Test
@SmallTest
fun centerScaled_horizontal() {
assertEquals(Rect(0, 0, 16, 8), Rect(4, 0, 12, 8).centerScaled(2F, 1F))
}
@Test
@SmallTest
fun centerScaled_vertical() {
assertEquals(Rect(0, 0, 8, 16), Rect(0, 4, 8, 12).centerScaled(1F, 2F))
}
@Test
@SmallTest
fun centerScaled_sameSquare() {
assertEquals(Rect(0, 0, 16, 16), Rect(4, 4, 12, 12).centerScaled(2F, 2F))
}
@Test
@SmallTest
fun centerScaled_sameRectangle() {
assertEquals(Rect(0, 0, 8, 16), Rect(2, 4, 6, 12).centerScaled(2F, 2F))
}
@Test
@SmallTest
fun intersectionWith_sameRectangle() {
assertEquals(Rect(0, 0, 15, 15), Rect(0, 0, 15, 15).intersectionWith(Rect(0, 0, 15, 15)))
}
@Test
@SmallTest
fun intersectionWith_parent() {
assertEquals(Rect(2, 2, 15, 15), Rect(2, 2, 15, 15).intersectionWith(Rect(0, 0, 17, 17)))
}
@Test
@SmallTest
fun intersectionWith_child() {
assertEquals(Rect(2, 2, 15, 15), Rect(0, 0, 17, 17).intersectionWith(Rect(2, 2, 15, 15)))
}
@Test
@SmallTest
fun intersectionWith_overlap() {
assertEquals(Rect(2, 2, 15, 15), Rect(0, 0, 15, 15).intersectionWith(Rect(2, 2, 17, 17)))
}
@Test
@SmallTest
fun intersectionWith_noOverlap() {
assertFailsWith(
"Given rects do not intersect",
fun () {
Rect(0, 0, 7, 7).intersectionWith(Rect(7, 7, 15, 15))
}
)
}
@Test
@SmallTest
fun move_vertical() {
assertEquals(Rect(0, 0, 15, 15), Rect(0, 2, 15, 17).move(0, -2))
}
@Test
@SmallTest
fun move_horizontal() {
assertEquals(Rect(0, 0, 15, 15), Rect(2, 0, 17, 15).move(-2, 0))
}
@Test
@SmallTest
fun move_both() {
assertEquals(Rect(2, 2, 15, 15), Rect(4, 0, 17, 13).move(-2, 2))
}
@Test
@SmallTest
fun projectRegionOfInterest_smaller() {
assertEquals(Rect(2, 14, 16, 28), Size(36, 84).projectRegionOfInterest(Size(18, 42), Rect(4, 28, 32, 56)))
}
@Test
@SmallTest
fun projectRegionOfInterest_exactFit() {
assertEquals(Rect(0, 0, 18, 42), Size(36, 84).projectRegionOfInterest(Size(18, 42), Rect(0, 0, 36, 84)))
}
@Test
@SmallTest
fun projectRegionOfInterest_larger() {
assertEquals(Rect(0, -1, 19, 42), Size(36, 84).projectRegionOfInterest(Size(18, 42), Rect(0, -2, 38, 84)))
}
@Test
@SmallTest
fun projectRegionOfInterest_noSize() {
assertFailsWith(
"Cannot project from container with non-positive dimensions",
fun () {
Size(0, 0).projectRegionOfInterest(Size(18, 42), Rect(0, -2, 38, 84))
}
)
}
@Test
@SmallTest
fun projectRegionOfInterest_offCenter() {
assertEquals(Rect(6, 14, 16, 20), Size(36, 84).projectRegionOfInterest(Size(18, 42), Rect(12, 28, 32, 40)))
}
@Test
@SmallTest
fun minAspectRatioSurroundingSize_squareVertical() {
assertEquals(Size(900, 1800), minAspectRatioSurroundingSize(Size(900, 900), 0.5F))
}
@Test
@SmallTest
fun minAspectRatioSurroundingSize_squareHorizontal() {
assertEquals(Size(1800, 900), minAspectRatioSurroundingSize(Size(900, 900), 2F))
}
@Test
@SmallTest
fun minAspectRatioSurroundingSize_rectangleVerticalToVertical() {
assertEquals(Size(900, 1800), minAspectRatioSurroundingSize(Size(900, 1100), 0.5F))
}
@Test
@SmallTest
fun minAspectRatioSurroundingSize_rectangleVerticalToHorizontal() {
assertEquals(Size(2200, 1100), minAspectRatioSurroundingSize(Size(900, 1100), 2F))
}
@Test
@SmallTest
fun minAspectRatioSurroundingSize_rectangleHorizontalToVertical() {
assertEquals(Size(1100, 2200), minAspectRatioSurroundingSize(Size(1100, 900), 0.5F))
}
@Test
@SmallTest
fun minAspectRatioSurroundingSize_rectangleHorizontalToHorizontal() {
assertEquals(Size(1800, 900), minAspectRatioSurroundingSize(Size(1100, 900), 2F))
}
@Test
@SmallTest
fun adjustSizeToAspectRatio_verticalCrop() {
assertEquals(Size(900, 1800), adjustSizeToAspectRatio(Size(900, 2200), 0.5F))
}
@Test
@SmallTest
fun adjustSizeToAspectRatio_verticalExpand() {
assertEquals(Size(900, 1800), adjustSizeToAspectRatio(Size(900, 1600), 0.5F))
}
@Test
@SmallTest
fun adjustSizeToAspectRatio_horizontalCrop() {
assertEquals(Size(1800, 900), adjustSizeToAspectRatio(Size(2200, 900), 2F))
}
@Test
@SmallTest
fun adjustSizeToAspectRatio_horizontalExpand() {
assertEquals(Size(1800, 900), adjustSizeToAspectRatio(Size(1600, 900), 2F))
}
}
================================================
FILE: scan-framework/src/androidTest/java/com/getbouncer/scan/framework/util/AppDetailsTest.kt
================================================
package com.getbouncer.scan.framework.util
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class AppDetailsTest {
private val testContext = InstrumentationRegistry.getInstrumentation().context
@Test
fun appDetails_full() {
val appDetails = AppDetails.fromContext(testContext)
assertEquals("com.getbouncer.scan.framework.test", appDetails.appPackageName)
assertEquals("", appDetails.applicationId)
assertEquals("com.getbouncer.scan.framework", appDetails.libraryPackageName)
assertTrue(appDetails.sdkVersion.startsWith("2."), "${appDetails.sdkVersion} does not start with \"2.\"")
assertEquals(-1, appDetails.sdkVersionCode)
assertTrue(appDetails.sdkFlavor.isNotEmpty())
}
}
================================================
FILE: scan-framework/src/main/AndroidManifest.xml
================================================
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/Analyzer.kt
================================================
package com.getbouncer.scan.framework
import java.io.Closeable
/**
* The default number of analyzers to run in parallel.
*/
internal const val DEFAULT_ANALYZER_PARALLEL_COUNT = 2
/**
* An analyzer takes some data as an input, and returns an analyzed output. Analyzers should not
* contain any state. They must define whether they can run on a multithreaded executor, and provide
* a means of analyzing input data to return some form of result.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
interface Analyzer {
suspend fun analyze(data: Input, state: State): Output
}
/**
* A factory to create analyzers.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
interface AnalyzerFactory> {
suspend fun newInstance(): AnalyzerType?
}
/**
* A pool of analyzers.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class AnalyzerPool(
val desiredAnalyzerCount: Int,
val analyzers: List>
) {
companion object {
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan")
)
suspend fun of(
analyzerFactory: AnalyzerFactory>,
desiredAnalyzerCount: Int = DEFAULT_ANALYZER_PARALLEL_COUNT,
) = AnalyzerPool(
desiredAnalyzerCount = desiredAnalyzerCount,
analyzers = (0 until desiredAnalyzerCount).mapNotNull { analyzerFactory.newInstance() }
)
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun closeAllAnalyzers() {
// This should be using analyzers.forEach, but doing so seems to require API 24. It's unclear why this won't use
// the kotlin.collections version of `forEach`, but it's not during compile.
for (it in analyzers) { if (it is Closeable) it.close() }
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/Config.kt
================================================
package com.getbouncer.scan.framework
import com.getbouncer.scan.framework.exception.InvalidBouncerApiKeyException
import com.getbouncer.scan.framework.time.Duration
import com.getbouncer.scan.framework.time.Rate
import com.getbouncer.scan.framework.time.seconds
import kotlinx.serialization.json.Json
private const val REQUIRED_API_KEY_LENGTH = 32
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
object Config {
/**
* If set to true, turns on debug information.
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var isDebug: Boolean = false
/**
* A log tag used by this library.
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var logTag: String = "Bouncer"
/**
* The API key to interface with Bouncer servers
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var apiKey: String? = null
set(value) {
if (value != null && value.length != REQUIRED_API_KEY_LENGTH) {
throw InvalidBouncerApiKeyException
}
field = value
}
/**
* The JSON configuration to use throughout this SDK.
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var json: Json = Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = true
}
/**
* Whether or not to track stats
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
val trackStats: Boolean = true
/**
* Whether or not to upload stats
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var uploadStats: Boolean = true
/**
* Whether or not to display the Bouncer logo
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var displayLogo: Boolean = true
/**
* Whether or not to display the result of the scan to the user
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var displayScanResult: Boolean = true
/**
* If set to true, opt-in to beta versions of the ML models.
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var betaModelOptIn: Boolean = false
/**
* The frame rate of a device that is considered slow will be below this rate.
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var slowDeviceFrameRate = Rate(2, 1.seconds)
/**
* Allow downloading ML models.
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var downloadModels = true
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
object NetworkConfig {
/**
* The base URL where all network requests will be sent
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var baseUrl = "https://api.getbouncer.com"
/**
* Whether or not to compress network request bodies.
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var useCompression: Boolean = false
/**
* The total number of times to try making a network request.
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var retryTotalAttempts: Int = 3
/**
* The delay between network request retries.
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var retryDelay: Duration = 5.seconds
/**
* Status codes that should be retried from bouncer servers.
*/
@JvmStatic
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
var retryStatusCodes: Iterable = 500..599
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/Fetcher.kt
================================================
package com.getbouncer.scan.framework
import android.content.Context
import android.util.Log
import com.getbouncer.scan.framework.api.NetworkResult
import com.getbouncer.scan.framework.api.downloadFileWithRetries
import com.getbouncer.scan.framework.api.getModelDetails
import com.getbouncer.scan.framework.api.getModelSignedUrl
import com.getbouncer.scan.framework.time.ClockMark
import com.getbouncer.scan.framework.time.asEpochMillisecondsClockMark
import com.getbouncer.scan.framework.time.days
import com.getbouncer.scan.framework.util.HashMismatchException
import com.getbouncer.scan.framework.util.calculateHash
import com.getbouncer.scan.framework.util.fileMatchesHash
import com.getbouncer.scan.framework.util.memoizeSuspend
import com.getbouncer.scan.framework.util.sanitizeFileName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.net.URL
import java.security.NoSuchAlgorithmException
private const val CACHE_MODEL_MAX_COUNT = 3
private const val PURPOSE_MODEL_UPGRADE = "model_upgrade"
/**
* Fetched data metadata.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
sealed class FetchedModelMeta(open val modelVersion: String, open val hashAlgorithm: String)
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class FetchedModelFileMeta(
override val modelVersion: String,
override val hashAlgorithm: String,
val modelFile: File?,
) : FetchedModelMeta(modelVersion, hashAlgorithm)
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class FetchedModelResourceMeta(
override val modelVersion: String,
override val hashAlgorithm: String,
val hash: String,
val assetFileName: String?,
) : FetchedModelMeta(modelVersion, hashAlgorithm)
/**
* Fetched data information.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
sealed class FetchedData(
open val modelClass: String,
open val modelFrameworkVersion: Int,
open val modelVersion: String,
open val modelHash: String?,
open val modelHashAlgorithm: String?,
) {
companion object {
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun fromFetchedModelMeta(modelClass: String, modelFrameworkVersion: Int, meta: FetchedModelMeta) = when (meta) {
is FetchedModelFileMeta ->
FetchedFile(
modelClass = modelClass,
modelFrameworkVersion = modelFrameworkVersion,
modelVersion = meta.modelVersion,
modelHash = meta.modelFile?.let { runBlocking { try { calculateHash(it, meta.hashAlgorithm) } catch (t: Throwable) { null } } },
modelHashAlgorithm = meta.hashAlgorithm,
file = meta.modelFile
)
is FetchedModelResourceMeta ->
FetchedResource(
modelClass = modelClass,
modelFrameworkVersion = modelFrameworkVersion,
modelVersion = meta.modelVersion,
modelHash = meta.hash,
modelHashAlgorithm = meta.hashAlgorithm,
assetFileName = meta.assetFileName,
)
}
}
abstract val successfullyFetched: Boolean
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class FetchedResource(
override val modelClass: String,
override val modelFrameworkVersion: Int,
override val modelVersion: String,
override val modelHash: String?,
override val modelHashAlgorithm: String?,
val assetFileName: String?,
) : FetchedData(modelClass, modelFrameworkVersion, modelVersion, modelHash, modelHashAlgorithm) {
override val successfullyFetched: Boolean = assetFileName != null
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class FetchedFile(
override val modelClass: String,
override val modelFrameworkVersion: Int,
override val modelVersion: String,
override val modelHash: String?,
override val modelHashAlgorithm: String?,
val file: File?,
) : FetchedData(modelClass, modelFrameworkVersion, modelVersion, modelHash, modelHashAlgorithm) {
override val successfullyFetched: Boolean = modelHash != null
}
/**
* An interface for getting data ready to be loaded into memory.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
interface Fetcher {
val modelClass: String
val modelFrameworkVersion: Int
/**
* Prepare data to be loaded into memory. If the fetched data is to be used immediately, the fetcher will prioritize
* fetching from the cache over getting the latest version.
*
* @param forImmediateUse: if there is a cached version of the model, return that immediately instead of downloading a new model
*/
suspend fun fetchData(forImmediateUse: Boolean, isOptional: Boolean): FetchedData
suspend fun isCached(): Boolean
}
/**
* A [Fetcher] that gets data from android resources.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
abstract class ResourceFetcher : Fetcher {
protected abstract val modelVersion: String
protected abstract val hash: String
protected abstract val hashAlgorithm: String
protected abstract val assetFileName: String
override suspend fun fetchData(forImmediateUse: Boolean, isOptional: Boolean): FetchedResource =
FetchedResource(
modelClass = modelClass,
modelFrameworkVersion = modelFrameworkVersion,
modelVersion = modelVersion,
modelHash = hash,
modelHashAlgorithm = hashAlgorithm,
assetFileName = assetFileName,
)
override suspend fun isCached(): Boolean = true
}
/**
* A [Fetcher] that downloads data from the web.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
sealed class WebFetcher(protected val context: Context) : Fetcher {
protected data class DownloadDetails(val url: URL, val hash: String, val hashAlgorithm: String, val modelVersion: String)
/**
* Keep track of any exceptions that occurred when fetching data after the specified number of retries. This is
* used to prevent the fetcher from repeatedly trying to fetch the data from multiple threads after the number of
* retries has been reached.
*/
private var fetchException: Throwable? = null
override suspend fun fetchData(forImmediateUse: Boolean, isOptional: Boolean): FetchedData {
val stat = Stats.trackPersistentRepeatingTask("web_fetcher_$modelClass")
val cachedData = FetchedData.fromFetchedModelMeta(modelClass, modelFrameworkVersion, tryFetchLatestCachedData())
// attempt to fetch the data from local cache if it's needed immediately or downloading is not allowed
if (forImmediateUse || !Config.downloadModels) {
tryFetchLatestCachedData().run {
val data = FetchedData.fromFetchedModelMeta(modelClass, modelFrameworkVersion, this)
if (data.successfullyFetched) {
Log.d(Config.logTag, "Fetcher: $modelClass is needed immediately and cached version ${data.modelVersion} is available.")
stat.trackResult("success")
return@fetchData data
}
}
}
// if downloading models is not allowed, return an empty fetched data
if (!Config.downloadModels) {
Log.d(Config.logTag, "Fetcher: $modelClass cannot be downloaded since downloads are turned off")
stat.trackResult("downloads_disabled")
return FetchedData.fromFetchedModelMeta(
modelClass = modelClass,
modelFrameworkVersion = modelFrameworkVersion,
meta = FetchedModelFileMeta(
modelVersion = cachedData.modelVersion,
hashAlgorithm = cachedData.modelHashAlgorithm ?: "",
modelFile = null,
),
)
}
// get details for downloading the data. If download details cannot be retrieved, use the latest cached version
val downloadDetails = fetchDownloadDetails(cachedData.modelHash, cachedData.modelHashAlgorithm) ?: run {
Log.d(Config.logTag, "Fetcher: not downloading $modelClass, using cached version ${cachedData.modelVersion}")
stat.trackResult("no_download_details")
return@fetchData cachedData
}
// if no cache is available, this is needed immediately, and this is optional, return a download failure
if (forImmediateUse && isOptional) {
Log.d(Config.logTag, "Fetcher: optional $modelClass needed for immediate use, but no cache available.")
stat.trackResult("optional_model_not_downloaded")
return FetchedData.fromFetchedModelMeta(
modelClass = modelClass,
modelFrameworkVersion = modelFrameworkVersion,
meta = FetchedModelFileMeta(
modelVersion = downloadDetails.modelVersion,
hashAlgorithm = downloadDetails.hashAlgorithm,
modelFile = null,
),
)
}
return try {
// check the local cache for a matching model
tryFetchMatchingCachedFile(downloadDetails.hash, downloadDetails.hashAlgorithm).run {
val data = FetchedData.fromFetchedModelMeta(modelClass, modelFrameworkVersion, this)
if (data.successfullyFetched) {
Log.d(Config.logTag, "Fetcher: $modelClass already has latest version downloaded.")
stat.trackResult("success_cached")
return@fetchData data
}
}
downloadData(downloadDetails).also {
if (it.successfullyFetched) {
Log.d(Config.logTag, "Fetcher: $modelClass successfully downloaded.")
stat.trackResult("success_downloaded")
} else {
Log.d(Config.logTag, "Fetcher: $modelClass failed to download from $downloadDetails.")
stat.trackResult("download_failed")
}
}
} catch (t: Throwable) {
fetchException = t
if (cachedData.successfullyFetched) {
Log.w(Config.logTag, "Fetcher: Failed to download model $modelClass, loaded from local cache", t)
stat.trackResult("success_download_failed_but_cached")
} else {
Log.e(Config.logTag, "Fetcher: Failed to download model $modelClass, no local cache available", t)
stat.trackResult(t::class.java.simpleName)
}
cachedData
}
}
override suspend fun isCached(): Boolean = when (val meta = tryFetchLatestCachedData()) {
is FetchedModelFileMeta -> meta.modelFile != null
is FetchedModelResourceMeta -> true
}
/**
* Get information about what version of the model to download.
*/
private val fetchDownloadDetails = memoizeSuspend(3.days) { cachedHash: String?, cachedHashAlgorithm: String? ->
getDownloadDetails(cachedHash, cachedHashAlgorithm)
}
/**
* Download the data using memoization so that data is only downloaded once.
*/
private val downloadData = memoizeSuspend { downloadDetails: DownloadDetails ->
val downloadOutputFile = getDownloadOutputFile(downloadDetails.modelVersion)
// if a previous exception was encountered, attempt to fetch cached data
fetchException?.run {
Log.d(Config.logTag, "Fetcher: Previous exception encountered for $modelClass, rethrowing")
throw this
}
try {
downloadAndVerify(
context = context,
url = downloadDetails.url,
outputFile = downloadOutputFile,
hash = downloadDetails.hash,
hashAlgorithm = downloadDetails.hashAlgorithm,
)
Log.d(Config.logTag, "Fetcher: $modelClass downloaded version ${downloadDetails.modelVersion}")
return@memoizeSuspend FetchedFile(
modelClass = modelClass,
modelFrameworkVersion = modelFrameworkVersion,
modelVersion = downloadDetails.modelVersion,
modelHash = downloadDetails.hash,
modelHashAlgorithm = downloadDetails.hashAlgorithm,
file = downloadOutputFile,
)
} finally {
cleanUpPostDownload(downloadOutputFile)
}
}
/**
* Attempt to load the data from the local cache.
*/
protected abstract suspend fun tryFetchLatestCachedData(): FetchedModelMeta
/**
* Attempt to load a cached data given the required [hash] and [hashAlgorithm].
*/
protected abstract suspend fun tryFetchMatchingCachedFile(hash: String, hashAlgorithm: String): FetchedModelMeta
/**
* Get [DownloadDetails] for the data that will be downloaded.
*
* @param cachedModelHash: the hash of the cached model, or null if nothing is cached
* @param cachedModelHashAlgorithm: the hash algorithm used to calculate the hash
*/
protected abstract suspend fun getDownloadDetails(
cachedModelHash: String?,
cachedModelHashAlgorithm: String?,
): DownloadDetails?
/**
* Get the file where the data should be downloaded.
*/
protected abstract suspend fun getDownloadOutputFile(modelVersion: String): File
/**
* After download, clean up.
*/
protected abstract suspend fun cleanUpPostDownload(downloadedFile: File)
/**
* Clear the cache for this loader. This will force new downloads.
*/
abstract suspend fun clearCache()
}
/**
* A [WebFetcher] that directly downloads a model.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
abstract class DirectDownloadWebFetcher(context: Context) : WebFetcher(context) {
abstract val url: URL
abstract val hash: String
abstract val hashAlgorithm: String
abstract val modelVersion: String
private val localFileName: String by lazy { url.path.replace('/', '_') }
override suspend fun tryFetchLatestCachedData(): FetchedModelMeta {
val localFile = getDownloadOutputFile(modelVersion)
return if (fileMatchesHash(localFile, hash, hashAlgorithm)) {
FetchedModelFileMeta(modelVersion, hashAlgorithm, localFile)
} else {
FetchedModelFileMeta(modelVersion, hashAlgorithm, null)
}
}
override suspend fun tryFetchMatchingCachedFile(hash: String, hashAlgorithm: String): FetchedModelMeta =
FetchedModelFileMeta(modelVersion, hashAlgorithm, null)
override suspend fun getDownloadOutputFile(modelVersion: String) =
File(context.cacheDir, sanitizeFileName(localFileName))
override suspend fun getDownloadDetails(
cachedModelHash: String?,
cachedModelHashAlgorithm: String?,
): DownloadDetails? =
DownloadDetails(url, hash, hashAlgorithm, modelVersion)
override suspend fun cleanUpPostDownload(downloadedFile: File) { /* nothing to do */ }
override suspend fun clearCache() {
val localFile = getDownloadOutputFile(modelVersion)
if (localFile.exists()) {
localFile.delete()
}
}
}
/**
* A [WebFetcher] that uses the signed URL server endpoints to download data.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
abstract class SignedUrlModelWebFetcher(context: Context) : DirectDownloadWebFetcher(context) {
abstract val modelFileName: String
private val localFileName by lazy { "${modelClass}_${modelFileName}_$modelVersion" }
// this field is not used by this class
override val url: URL = URL(NetworkConfig.baseUrl)
override suspend fun getDownloadOutputFile(modelVersion: String) = File(context.cacheDir, sanitizeFileName(localFileName))
override suspend fun getDownloadDetails(
cachedModelHash: String?,
cachedModelHashAlgorithm: String?,
) = when (val signedUrlResponse = getModelSignedUrl(context, modelClass, modelVersion, modelFileName)) {
is NetworkResult.Success ->
try {
URL(signedUrlResponse.body.modelUrl)
} catch (t: Throwable) {
Log.e(Config.logTag, "Fetcher: Invalid signed url for model $modelClass: ${signedUrlResponse.body.modelUrl}", t)
null
}
is NetworkResult.Error -> {
Log.w(Config.logTag, "Fetcher: Failed to get signed url for model $modelClass: ${signedUrlResponse.error}")
null
}
is NetworkResult.Exception -> {
Log.e(Config.logTag, "Fetcher: Exception fetching signed url for model $modelClass: ${signedUrlResponse.responseCode}", signedUrlResponse.exception)
null
}
}?.let { DownloadDetails(it, hash, hashAlgorithm, modelVersion) }
}
/**
* A [WebFetcher] that queries Bouncer servers for updated data. If a new version is found, download it. If the data
* details match what is cached, return the cached version instead.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
abstract class UpdatingModelWebFetcher(context: Context) : SignedUrlModelWebFetcher(context) {
abstract val defaultModelVersion: String
abstract val defaultModelFileName: String
abstract val defaultModelHash: String
abstract val defaultModelHashAlgorithm: String
private var cachedDownloadDetails: DownloadDetails? = null
private val getCacheFolder = memoizeSuspend {
ensureLocalFolder("${modelClass}_$modelFrameworkVersion")
}
override val modelVersion: String by lazy { defaultModelVersion }
override val modelFileName: String by lazy { defaultModelFileName }
override val hash: String by lazy { defaultModelHash }
override val hashAlgorithm: String by lazy { defaultModelHashAlgorithm }
override suspend fun tryFetchLatestCachedData(): FetchedModelMeta =
getLatestFile()?.let { FetchedModelFileMeta(it.name, defaultModelHashAlgorithm, it) } ?: FetchedModelFileMeta(defaultModelVersion, defaultModelHashAlgorithm, null)
override suspend fun tryFetchMatchingCachedFile(hash: String, hashAlgorithm: String): FetchedModelMeta =
getMatchingFile(hash, hashAlgorithm)?.let { FetchedModelFileMeta(it.name, defaultModelHashAlgorithm, it) } ?: FetchedModelFileMeta(defaultModelVersion, defaultModelHashAlgorithm, null)
override suspend fun getDownloadOutputFile(modelVersion: String) =
File(getCacheFolder(), sanitizeFileName(modelVersion))
override suspend fun getDownloadDetails(
cachedModelHash: String?,
cachedModelHashAlgorithm: String?,
): DownloadDetails? {
cachedDownloadDetails?.let { return DownloadDetails(url, hash, hashAlgorithm, modelVersion) }
val nextUpgradeTime = getNextUpgradeTime()
when {
Config.betaModelOptIn ->
Log.d(Config.logTag, "Fetcher: Beta opt-in, attempting to upgrade $modelClass")
nextUpgradeTime.hasPassed() ->
Log.d(Config.logTag, "Fetcher: Time to upgrade $modelClass, fetching upgrade details")
cachedModelHash == null ->
Log.d(Config.logTag, "Fetcher: Downloading initial version of $modelClass")
else -> {
Log.d(Config.logTag, "Fetcher: Not yet time to upgrade $modelClass (will upgrade at $nextUpgradeTime)")
return null
}
}
return when (
val detailsResponse = getModelDetails(
context = context,
modelClass = modelClass,
modelFrameworkVersion = modelFrameworkVersion,
cachedModelHash = cachedModelHash,
cachedModelHashAlgorithm = cachedModelHashAlgorithm,
)
) {
is NetworkResult.Success ->
try {
detailsResponse.body.queryAgainAfterMs?.asEpochMillisecondsClockMark()?.apply {
setNextModelUpgradeAttemptTime(this)
}
detailsResponse.body.url?.let {
DownloadDetails(
url = URL(it),
hash = detailsResponse.body.hash,
hashAlgorithm = detailsResponse.body.hashAlgorithm,
modelVersion = detailsResponse.body.modelVersion,
).apply { cachedDownloadDetails = this }
}
} catch (t: Throwable) {
Log.e(Config.logTag, "Fetcher: Invalid signed url for model $modelClass: ${detailsResponse.body.url}", t)
null
}
is NetworkResult.Error -> {
Log.w(Config.logTag, "Fetcher: Failed to get latest details for model $modelClass: ${detailsResponse.error}")
fallbackDownloadDetails()
}
is NetworkResult.Exception -> {
Log.e(Config.logTag, "Fetcher: Exception retrieving latest details for model $modelClass: ${detailsResponse.responseCode}", detailsResponse.exception)
fallbackDownloadDetails()
}
}
}
/**
* Determine if we should query for a model upgrade
*/
protected open fun getNextUpgradeTime(): ClockMark =
StorageFactory
.getStorageInstance(context, PURPOSE_MODEL_UPGRADE)
.getLong(modelClass, 0)
.asEpochMillisecondsClockMark()
protected open fun setNextModelUpgradeAttemptTime(time: ClockMark) {
StorageFactory
.getStorageInstance(context, PURPOSE_MODEL_UPGRADE)
.storeValue(modelClass, time.toMillisecondsSinceEpoch())
}
protected open fun clearNextUpgradeTime() {
StorageFactory
.getStorageInstance(context, PURPOSE_MODEL_UPGRADE)
.remove(modelClass)
}
/**
* Fall back to getting the download details.
*/
protected open suspend fun fallbackDownloadDetails() =
super.getDownloadDetails(null, null)?.apply { cachedDownloadDetails = this }
/**
* Delete all files in cache that are not the recently downloaded file.
*/
override suspend fun cleanUpPostDownload(downloadedFile: File) = withContext(Dispatchers.IO) {
try {
getCacheFolder()
.listFiles()
?.filter { it != downloadedFile && calculateHash(it, defaultModelHashAlgorithm) != defaultModelHash }
?.sortedByDescending { it.lastModified() }
?.filterIndexed { index, _ -> index > CACHE_MODEL_MAX_COUNT }
?.forEach { it.delete() }
} catch (t: Throwable) {
Log.e(Config.logTag, "Error cleaning up post download", t)
}.let { }
}
/**
* If a file in the cache directory matches the provided [hash], return it.
*/
private suspend fun getMatchingFile(hash: String, hashAlgorithm: String): File? =
withContext(Dispatchers.IO) {
try {
getCacheFolder()
.listFiles()
?.sortedByDescending { it.lastModified() }
?.firstOrNull { calculateHash(it, hashAlgorithm) == hash }
} catch (t: Throwable) {
Log.e(Config.logTag, "Unable to get matching file", t)
null
}
}
/**
* Get the highest model version, or most recently created file in the cache folder. Return null
* if no files in cache
*/
private suspend fun getLatestFile(): File? = withContext(Dispatchers.IO) {
val files = getCacheFolder()
.listFiles()
?.filter { it.exists() && it.length() > 0 }
files
?.filter { it.name.startsWith("1.") }
?.mapNotNull { file -> ModelVersion.fromString(file.name)?.let { it to file } }
?.maxByOrNull { it.first }
?.second ?: files?.maxByOrNull { it.lastModified() }
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class ModelVersion(
val versioningVersion: Int,
val frameworkVersion: Int,
val modelNumber: Int,
val quantization: Int,
) : Comparable {
override fun compareTo(other: ModelVersion): Int {
val versioningDiff = versioningVersion.compareTo(other.versioningVersion)
val frameworkDiff = frameworkVersion.compareTo(other.frameworkVersion)
val modelDiff = modelNumber.compareTo(other.modelNumber)
return when {
versioningDiff != 0 -> versioningDiff
frameworkDiff != 0 -> frameworkDiff
modelDiff != 0 -> modelDiff
else -> 0
}
}
companion object {
fun fromString(modelVersion: String): ModelVersion? {
val components = modelVersion.split("\\.").mapNotNull {
try { it.toInt() } catch (t: Throwable) { null }
}
if (components.size != 4) {
return null
}
return ModelVersion(
components[0],
components[1],
components[2],
components[3],
)
}
}
}
/**
* Ensure that the local folder exists and get it.
*/
private suspend fun ensureLocalFolder(folderName: String): File = withContext(Dispatchers.IO) {
val localFolder = File(context.cacheDir, folderName)
if (localFolder.exists() && !localFolder.isDirectory) {
localFolder.delete()
}
if (!localFolder.exists()) {
localFolder.mkdirs()
}
localFolder
}
/**
* Force re-download of models by clearing the cache.
*/
override suspend fun clearCache() = withContext(Dispatchers.IO) {
getCacheFolder().deleteRecursively()
getCacheFolder().mkdirs()
clearNextUpgradeTime()
}.let { }
}
/**
* A [WebFetcher] that queries Bouncer servers for updated data. If a new version is found, download it. If the data
* details match what is cached, return the cached version instead.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
abstract class UpdatingResourceFetcher(context: Context) : UpdatingModelWebFetcher(context) {
protected abstract val assetFileName: String
protected abstract val resourceModelVersion: String
protected abstract val resourceModelHash: String
protected abstract val resourceModelHashAlgorithm: String
override val defaultModelFileName: String = ""
override val defaultModelVersion: String by lazy { resourceModelVersion }
override val defaultModelHash: String by lazy { resourceModelHash }
override val defaultModelHashAlgorithm: String by lazy { resourceModelHashAlgorithm }
override suspend fun tryFetchLatestCachedData() = super.tryFetchLatestCachedData().run {
when (this) {
is FetchedModelFileMeta -> if (modelFile == null) fetchModelFromResource() else this
is FetchedModelResourceMeta -> this
}
}
override suspend fun tryFetchMatchingCachedFile(hash: String, hashAlgorithm: String): FetchedModelMeta =
if (hash == defaultModelHash && hashAlgorithm == defaultModelHashAlgorithm) {
fetchModelFromResource()
} else {
super.tryFetchMatchingCachedFile(hash, hashAlgorithm)
}
override suspend fun fallbackDownloadDetails(): DownloadDetails? = DownloadDetails(
url = URL("https://localhost"),
hash = resourceModelHash,
hashAlgorithm = resourceModelHashAlgorithm,
modelVersion = resourceModelVersion,
)
private fun fetchModelFromResource(): FetchedModelMeta =
FetchedModelResourceMeta(
modelVersion = resourceModelVersion,
assetFileName = assetFileName,
hash = resourceModelHash,
hashAlgorithm = resourceModelHashAlgorithm,
)
}
/**
* Download a file from a given [url] and ensure that it matches the expected [hash].
*/
@Throws(IOException::class, NoSuchAlgorithmException::class, HashMismatchException::class)
private suspend fun downloadAndVerify(
context: Context,
url: URL,
outputFile: File,
hash: String,
hashAlgorithm: String
) {
downloadFile(context, url, outputFile)
val calculatedHash = calculateHash(outputFile, hashAlgorithm)
if (hash != calculatedHash) {
withContext(Dispatchers.IO) { outputFile.delete() }
throw HashMismatchException(hashAlgorithm, hash, calculatedHash)
}
}
/**
* Download a file from the provided [url] into the provided [outputFile].
*/
@Throws(IOException::class, FileAlreadyExistsException::class, NoSuchFileException::class)
private suspend fun downloadFile(context: Context, url: URL, outputFile: File) = withContext(Dispatchers.IO) {
if (outputFile.exists()) {
outputFile.delete()
}
downloadFileWithRetries(context, url, outputFile)
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/Loader.kt
================================================
package com.getbouncer.scan.framework
import android.content.Context
import android.util.Log
import com.getbouncer.scan.framework.ml.trackModelLoaded
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
/**
* An interface for loading data into a byte buffer.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class Loader(private val context: Context) {
/**
* Load previously fetched data into memory.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
suspend fun loadData(fetchedData: FetchedData): ByteBuffer? = when (fetchedData) {
is FetchedResource -> loadResourceData(fetchedData)
is FetchedFile -> loadFileData(fetchedData)
}
/**
* Create a [ByteBuffer] object from an android resource.
*/
private suspend fun loadResourceData(fetchedData: FetchedResource): ByteBuffer? {
val stat = Stats.trackPersistentRepeatingTask("resource_loader:${fetchedData.modelClass}")
if (fetchedData.assetFileName == null) {
trackModelLoaded(fetchedData.modelClass, fetchedData.modelVersion, fetchedData.modelFrameworkVersion, false)
stat.trackResult("failure:${fetchedData.modelClass}")
return null
}
return try {
val loadedData = readAssetToByteBuffer(context, fetchedData.assetFileName)
stat.trackResult("success")
trackModelLoaded(fetchedData.modelClass, fetchedData.modelVersion, fetchedData.modelFrameworkVersion, true)
loadedData
} catch (t: Throwable) {
Log.e(Config.logTag, "Failed to load resource", t)
trackModelLoaded(fetchedData.modelClass, fetchedData.modelVersion, fetchedData.modelFrameworkVersion, false)
null
}
}
/**
* Create a [ByteBuffer] object from a [File].
*/
private suspend fun loadFileData(fetchedData: FetchedFile): ByteBuffer? {
val stat = Stats.trackPersistentRepeatingTask("web_loader:${fetchedData.modelClass}")
if (fetchedData.file == null) {
trackModelLoaded(fetchedData.modelClass, fetchedData.modelVersion, fetchedData.modelFrameworkVersion, false)
stat.trackResult("failure:${fetchedData.modelClass}")
return null
}
return try {
val loadedData = readFileToByteBuffer(fetchedData.file)
stat.trackResult("success")
trackModelLoaded(fetchedData.modelClass, fetchedData.modelVersion, fetchedData.modelFrameworkVersion, true)
loadedData
} catch (t: Throwable) {
stat.trackResult("failure:${fetchedData.modelClass}")
trackModelLoaded(fetchedData.modelClass, fetchedData.modelVersion, fetchedData.modelFrameworkVersion, true)
null
}
}
}
/**
* Read a [File] into a [ByteBuffer].
*/
private suspend fun readFileToByteBuffer(file: File) = withContext(Dispatchers.IO) {
FileInputStream(file).use { readFileToByteBuffer(it, 0, file.length()) }
}
/**
* Read a raw resource into a [ByteBuffer].
*/
private suspend fun readAssetToByteBuffer(context: Context, assetFileName: String) =
withContext(Dispatchers.IO) {
context.assets.openFd(assetFileName).use { fileDescriptor ->
FileInputStream(fileDescriptor.fileDescriptor).use { input ->
readFileToByteBuffer(
input,
fileDescriptor.startOffset,
fileDescriptor.declaredLength,
)
}
}
}
/**
* Read a [fileInputStream] into a [ByteBuffer].
*/
@Throws(IOException::class)
private fun readFileToByteBuffer(
fileInputStream: FileInputStream,
startOffset: Long,
declaredLength: Long
): ByteBuffer = fileInputStream.channel.map(
FileChannel.MapMode.READ_ONLY,
startOffset,
declaredLength
)
/**
* Determine if an asset file exists
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun assetFileExists(context: Context, assetFileName: String) =
try {
context.assets.openFd(assetFileName).use { it.declaredLength > 0 }
} catch (t: Throwable) {
false
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/Loop.kt
================================================
package com.getbouncer.scan.framework
import com.getbouncer.scan.framework.time.Clock
import com.getbouncer.scan.framework.time.ClockMark
import com.getbouncer.scan.framework.time.Duration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
object NoAnalyzersAvailableException : Exception()
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
object AlreadySubscribedException : Exception()
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
interface AnalyzerLoopErrorListener {
/**
* A failure occurred during frame analysis. If this returns true, the loop will terminate. If this returns false,
* the loop will continue to execute on new data.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun onAnalyzerFailure(t: Throwable): Boolean
/**
* A failure occurred while collecting the result of frame analysis. If this returns true, the loop will terminate.
* If this returns false, the loop will continue to execute on new data.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun onResultFailure(t: Throwable): Boolean
}
/**
* A loop to execute repeated analysis. The loop uses coroutines to run the [Analyzer.analyze] method. If the [Analyzer]
* is threadsafe, multiple coroutines will be used. If not, a single coroutine will be used.
*
* Any data enqueued while the analyzers are at capacity will be dropped.
*
* This will process data until the result aggregator returns true.
*
* Note: an analyzer loop can only be started once. Once it terminates, it cannot be restarted.
*
* @param analyzerPool: A pool of analyzers to use in this loop.
* @param analyzerLoopErrorListener: An error handler for this loop
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
sealed class AnalyzerLoop(
private val analyzerPool: AnalyzerPool,
private val analyzerLoopErrorListener: AnalyzerLoopErrorListener,
) : ResultHandler {
private val started = AtomicBoolean(false)
protected var startedAt: ClockMark? = null
private var finished: Boolean = false
private val cancelMutex = Mutex()
private lateinit var loopExecutionStatTracker: StatTracker
private var workerJob: Job? = null
protected fun subscribeToFlow(flow: Flow, processingCoroutineScope: CoroutineScope): Job? {
if (!started.getAndSet(true)) {
startedAt = Clock.markNow()
} else {
analyzerLoopErrorListener.onAnalyzerFailure(AlreadySubscribedException)
return null
}
loopExecutionStatTracker = Stats.trackTask("${this::class.java.simpleName}_execution")
if (analyzerPool.analyzers.isEmpty()) {
processingCoroutineScope.launch { loopExecutionStatTracker.trackResult("canceled") }
analyzerLoopErrorListener.onAnalyzerFailure(NoAnalyzersAvailableException)
return null
}
workerJob = processingCoroutineScope.launch {
// This should be using analyzerPool.analyzers.forEach, but doing so seems to require API 24. It's unclear
// why this won't use the kotlin.collections version of `forEach`, but it's not during compile.
for (it in analyzerPool.analyzers) {
launch(Dispatchers.Default) {
startWorker(flow, it)
}
}
}
return workerJob
}
protected suspend fun unsubscribeFromFlow() = cancelMutex.withLock {
workerJob?.apply { if (isActive) { cancel() } }
workerJob = null
started.set(false)
finished = false
}
/**
* Launch a worker coroutine that has access to the analyzer's `analyze` method and the result handler
*/
private suspend fun startWorker(
flow: Flow,
analyzer: Analyzer,
) {
flow.collect { frame ->
val stat = Stats.trackRepeatingTask("analyzer_execution:${analyzer::class.java.simpleName}")
try {
val analyzerResult = analyzer.analyze(frame, getState())
try {
finished = onResult(analyzerResult, frame)
} catch (t: Throwable) {
stat.trackResult("result_failure")
handleResultFailure(t)
}
} catch (t: Throwable) {
stat.trackResult("analyzer_failure")
handleAnalyzerFailure(t)
}
if (finished) {
loopExecutionStatTracker.trackResult("success")
unsubscribeFromFlow()
}
stat.trackResult("success")
}
}
private suspend fun handleAnalyzerFailure(t: Throwable) {
if (withContext(Dispatchers.Main) { analyzerLoopErrorListener.onAnalyzerFailure(t) }) { unsubscribeFromFlow() }
}
private suspend fun handleResultFailure(t: Throwable) {
if (withContext(Dispatchers.Main) { analyzerLoopErrorListener.onResultFailure(t) }) { unsubscribeFromFlow() }
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
abstract fun getState(): State
}
/**
* This kind of [AnalyzerLoop] will process data until the result handler indicates that it has reached a terminal
* state and is no longer listening.
*
* Data can be added to a queue for processing by a camera or other producer. It will be consumed by FILO. If no data
* is available, the analyzer pauses until data becomes available.
*
* If the enqueued data exceeds the allowed memory size, the bottom of the data stack will be dropped and will not be
* processed. This alleviates memory pressure when producers are faster than the consuming analyzer.
*
* @param analyzerPool: A pool of analyzers to use in this loop.
* @param resultHandler: A result handler that will be called with the results from the analyzers in this loop.
* @param analyzerLoopErrorListener: An error handler for this loop
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class ProcessBoundAnalyzerLoop(
private val analyzerPool: AnalyzerPool,
private val resultHandler: StatefulResultHandler,
analyzerLoopErrorListener: AnalyzerLoopErrorListener
) : AnalyzerLoop(
analyzerPool,
analyzerLoopErrorListener,
) {
/**
* Subscribe to a flow. Loops can only subscribe to a single flow at a time.
*/
fun subscribeTo(flow: Flow, processingCoroutineScope: CoroutineScope) =
subscribeToFlow(flow, processingCoroutineScope)
/**
* Unsubscribe from the flow.
*/
fun unsubscribe() = runBlocking { unsubscribeFromFlow() }
override suspend fun onResult(result: Output, data: DataFrame) = resultHandler.onResult(result, data)
override fun getState(): State = resultHandler.state
}
/**
* This kind of [AnalyzerLoop] will process data provided as part of its constructor. Data will be processed in the
* order provided.
*
* @param analyzerPool: A pool of analyzers to use in this loop.
* @param resultHandler: A result handler that will be called with the results from the analyzers in this loop.
* @param analyzerLoopErrorListener: An error handler for this loop
* @param timeLimit: If specified, this is the maximum allowed time for the loop to run. If the loop
* exceeds this duration, the loop will terminate
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class FiniteAnalyzerLoop(
private val analyzerPool: AnalyzerPool,
private val resultHandler: TerminatingResultHandler,
analyzerLoopErrorListener: AnalyzerLoopErrorListener,
private val timeLimit: Duration = Duration.INFINITE
) : AnalyzerLoop(
analyzerPool,
analyzerLoopErrorListener,
) {
private val framesProcessed: AtomicInteger = AtomicInteger(0)
private var framesToProcess = 0
fun process(frames: Collection, processingCoroutineScope: CoroutineScope): Job? {
val channel = Channel(capacity = frames.size)
// TODO: upgrade this when kotlin libs hit 1.5.0
// framesToProcess = frames.map { channel.trySend(it) }.count { it.isSuccess }
@Suppress("DEPRECATION")
framesToProcess = frames.map { channel.offer(it) }.count { it }
return if (framesToProcess > 0) {
subscribeToFlow(channel.receiveAsFlow(), processingCoroutineScope)
} else {
processingCoroutineScope.launch { resultHandler.onAllDataProcessed() }
}
}
fun cancel() = runBlocking { unsubscribeFromFlow() }
override suspend fun onResult(result: Output, data: DataFrame): Boolean {
val framesProcessed = this.framesProcessed.incrementAndGet()
val timeElapsed = startedAt?.elapsedSince() ?: Duration.ZERO
resultHandler.onResult(result, data)
if (framesProcessed >= framesToProcess) {
resultHandler.onAllDataProcessed()
unsubscribeFromFlow()
} else if (timeElapsed > timeLimit) {
resultHandler.onTerminatedEarly()
unsubscribeFromFlow()
}
val allFramesProcessed = framesProcessed >= framesToProcess
val exceededTimeLimit = timeElapsed > timeLimit
return allFramesProcessed || exceededTimeLimit
}
override fun getState(): State = resultHandler.state
}
/**
* Consume this [Flow] using a channelFlow with no buffer. Elements emitted from [this] flow are offered to the
* underlying [channelFlow]. If the consumer is not currently suspended and waiting for the next element, the element is
* dropped.
*
* example:
* ```
* flow {
* (0..100).forEach {
* emit(it)
* delay(100)
* }
* }.backPressureDrop().collect {
* delay(1000)
* println(it)
* }
* ```
*
* @return a flow that only emits elements when the downstream [Flow.collect] is waiting for the next element
*/
@ExperimentalCoroutinesApi
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
suspend fun Flow.backPressureDrop(): Flow =
// TODO: upgrade this when kotlin libs hit 1.5.0
// channelFlow { this@backPressureDrop.collect { trySend(it) } }.buffer(capacity = Channel.RENDEZVOUS)
@Suppress("DEPRECATION")
channelFlow { this@backPressureDrop.collect { offer(it) } }.buffer(capacity = Channel.RENDEZVOUS)
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/MachineState.kt
================================================
package com.getbouncer.scan.framework
import android.util.Log
import com.getbouncer.scan.framework.time.Clock
import com.getbouncer.scan.framework.time.ClockMark
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
abstract class MachineState {
/**
* Keep track of when this state was reached
*/
protected open val reachedStateAt: ClockMark = Clock.markNow()
override fun toString(): String = "${this::class.java.simpleName}(reachedStateAt=$reachedStateAt)"
init {
if (Config.isDebug) Log.d(Config.logTag, "${this::class.java.simpleName} machine state reached")
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/Result.kt
================================================
package com.getbouncer.scan.framework
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import com.getbouncer.scan.framework.util.FrameRateTracker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
/**
* A result handler for data processing. This is called when results are available from an [Analyzer].
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
interface ResultHandler {
suspend fun onResult(result: Output, data: Input): Verdict
}
/**
* A specialized result handler that has some form of state.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
abstract class StatefulResultHandler(
private var initialState: State
) : ResultHandler {
/**
* The state of the result handler. This can be read, but not updated by analyzers.
*/
var state: State = initialState
protected set
/**
* Reset the state to the initial value.
*/
protected open fun reset() { state = initialState }
}
/**
* A result handler with a method that notifies when all data has been processed.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
abstract class TerminatingResultHandler(
initialState: State
) : StatefulResultHandler(initialState) {
/**
* All data has been processed and termination was reached.
*/
abstract suspend fun onAllDataProcessed()
/**
* Not all data was processed before termination.
*/
abstract suspend fun onTerminatedEarly()
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
interface AggregateResultListener {
/**
* The aggregated result of an [AnalyzerLoop] is available.
*
* @param result: the result from the Aggregator
*/
suspend fun onResult(result: FinalResult)
/**
* An interim result is available, but the [AnalyzerLoop] is still processing more data frames. This is useful for
* displaying a debug window or handling state updates during a scan.
*
* @param result: the result from the [AnalyzerLoop]
*/
suspend fun onInterimResult(result: InterimResult)
/**
* The result aggregator was reset back to its original state.
*/
suspend fun onReset()
}
/**
* The [ResultAggregator] processes results from analyzers until a condition is met. That condition is part of the
* aggregator's logic.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
abstract class ResultAggregator(
private val listener: AggregateResultListener,
private val initialState: State
) : StatefulResultHandler(initialState), LifecycleObserver {
private var isCanceled = false
private var isPaused = false
private var isFinished = false
private val aggregatorExecutionStats = runBlocking {
Stats.trackRepeatingTask("${this@ResultAggregator::class.java.simpleName}_aggregator_execution")
}
protected open val frameRateTracker by lazy { FrameRateTracker(this::class.java.simpleName) }
/**
* Reset the state of the aggregator and pause aggregation. This is useful for aggregators that can be backgrounded.
* For example, a user that is scanning an object, but then backgrounds the scanning app. In the case that the scan
* should be restarted, this feature pauses the result handlers and resets the state.
*/
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
private fun resetAndPause() {
reset()
isPaused = true
}
/**
* Resume aggregation after it has been paused.
*/
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
private fun resume() {
isPaused = false
}
/**
* Cancel a result aggregator. This means that the result aggregator will ignore all further results and will never
* return a final result.
*/
fun cancel() {
reset()
isCanceled = true
}
/**
* Bind this result aggregator to a lifecycle. This allows the result aggregator to pause and reset when the
* lifecycle owner pauses.
*/
open fun bindToLifecycle(lifecycleOwner: LifecycleOwner) {
lifecycleOwner.lifecycle.addObserver(this)
}
/**
* Reset the state of the aggregator. This is useful for aggregating data that can become invalid, such as when a
* user is scanning an object, and moves the object away from the camera before the scan has completed.
*/
override fun reset() {
super.reset()
isPaused = false
isCanceled = false
isFinished = false
state = initialState
frameRateTracker.reset()
runBlocking { listener.onReset() }
}
override suspend fun onResult(result: AnalyzerResult, data: DataFrame): Boolean = when {
isPaused -> false
isCanceled || isFinished -> true
else -> withContext(Dispatchers.Default) {
frameRateTracker.trackFrameProcessed()
val (interimResult, finalResult) = aggregateResult(data, result)
launch { listener.onInterimResult(interimResult) }
aggregatorExecutionStats.trackResult("frame_processed")
finalResult?.also {
isFinished = true
launch { listener.onResult(it) }
} != null
}
}
/**
* Aggregate a new result. If this method returns a non-null [FinalResult], the aggregator will stop listening for
* new results.
*
* @param result: The result to aggregate
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
abstract suspend fun aggregateResult(frame: DataFrame, result: AnalyzerResult): Pair
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/Scan.kt
================================================
package com.getbouncer.scan.framework
import android.os.Build
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
object Scan {
/**
* Determine if the device is running an ARM architecture.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun isDeviceArchitectureArm(): Boolean {
val arch = System.getProperty("os.arch") ?: ""
return "86" !in arch
}
/**
* Determine the architecture of the device.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun getDeviceArchitecture(): String? {
// From https://stackoverflow.com/questions/11989629/api-call-to-get-processor-architecture
// Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64"
// while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo).
// Instead we search through the supported abi:s on the device, see:
// http://developer.android.com/ndk/guides/abis.html
// Note that we search for abi:s in preferred order (the ordering of the
// Build.SUPPORTED_ABIS list) to avoid e.g. installing arm on an x86 system where arm
// emulation is available.
for (androidArch in Build.SUPPORTED_ABIS) {
when (androidArch) {
"arm64-v8a" -> return "aarch64"
"armeabi-v7a" -> return "arm"
"x86_64" -> return "x86_64"
"x86" -> return "i686"
}
}
return null
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/Stat.kt
================================================
package com.getbouncer.scan.framework
import android.util.Log
import androidx.annotation.CheckResult
import com.getbouncer.scan.framework.time.Clock
import com.getbouncer.scan.framework.time.ClockMark
import com.getbouncer.scan.framework.time.Duration
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.UUID
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
object Stats {
val instanceId = UUID.randomUUID().toString()
var scanId: String? = null
private set
private var persistentRepeatingTasks: MutableMap> = mutableMapOf()
private var tasks: MutableMap> = mutableMapOf()
private var repeatingTasks: MutableMap> = mutableMapOf()
private val scanIdMutex = Mutex()
private val taskMutex = Mutex()
private val repeatingTaskMutex = Mutex()
private val persistentRepeatingTasksMutex = Mutex()
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun startScan() {
runBlocking {
scanIdMutex.withLock {
clearAllTasks()
scanId = UUID.randomUUID().toString()
}
}
}
/**
* Track the duration of a task.
*/
@CheckResult
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun trackTask(name: String): StatTracker =
if (!Config.trackStats) StatTrackerNoOpImpl else StatTrackerImpl { startedAt, result ->
taskMutex.withLock {
val list = tasks[name]
if (list == null) {
tasks[name] = listOf(TaskStats(startedAt, startedAt.elapsedSince(), result))
} else {
tasks[name] = list + TaskStats(startedAt, startedAt.elapsedSince(), result)
}
}
if (Config.isDebug) {
Log.v(Config.logTag, "Task $name got result $result after ${startedAt.elapsedSince()}")
}
}
/**
* Track the result of a task.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
suspend fun trackTask(name: String, task: suspend () -> T): T {
val tracker = trackTask(name)
val result: T
try {
result = task()
tracker.trackResult("success")
} catch (t: Throwable) {
tracker.trackResult(t::class.java.simpleName)
throw t
}
return result
}
/**
* Track a single execution of a repeating task.
*/
@CheckResult
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun trackRepeatingTask(name: String): StatTracker =
if (!Config.trackStats) StatTrackerNoOpImpl else StatTrackerImpl { startedAt, result ->
repeatingTaskMutex.withLock {
val resultName = result ?: "null"
val resultStats = repeatingTasks[name] ?: run {
val taskStats = mutableMapOf()
repeatingTasks[name] = taskStats
taskStats
}
val taskStats = resultStats[resultName]
val duration = startedAt.elapsedSince()
if (taskStats == null) {
resultStats[resultName] = RepeatingTaskStats(
executions = 1,
startedAt = startedAt,
totalDuration = duration,
totalCpuDuration = duration,
minimumDuration = duration,
maximumDuration = duration,
)
} else {
resultStats[resultName] = RepeatingTaskStats(
executions = taskStats.executions + 1,
startedAt = taskStats.startedAt,
totalDuration = taskStats.startedAt.elapsedSince(),
totalCpuDuration = taskStats.totalCpuDuration + duration,
minimumDuration = minOf(taskStats.minimumDuration, duration),
maximumDuration = maxOf(taskStats.maximumDuration, duration),
)
}
}
if (Config.isDebug) {
Log.v(Config.logTag, "Repeating task $name got result $result after ${startedAt.elapsedSince()}")
}
}
/**
* Track a single execution of a repeating task that should not be cleared on scan start.
*/
@CheckResult
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun trackPersistentRepeatingTask(name: String): StatTracker =
if (!Config.trackStats) StatTrackerNoOpImpl else StatTrackerImpl { startedAt, result ->
persistentRepeatingTasksMutex.withLock {
val resultName = result ?: "null"
val resultStats = persistentRepeatingTasks[name] ?: run {
val taskStats = mutableMapOf()
persistentRepeatingTasks[name] = taskStats
taskStats
}
val taskStats = resultStats[resultName]
val duration = startedAt.elapsedSince()
if (taskStats == null) {
resultStats[resultName] = RepeatingTaskStats(
executions = 1,
startedAt = startedAt,
totalDuration = duration,
totalCpuDuration = duration,
minimumDuration = duration,
maximumDuration = duration,
)
} else {
resultStats[resultName] = RepeatingTaskStats(
executions = taskStats.executions + 1,
startedAt = taskStats.startedAt,
totalDuration = taskStats.startedAt.elapsedSince(),
totalCpuDuration = taskStats.totalCpuDuration + duration,
minimumDuration = minOf(taskStats.minimumDuration, duration),
maximumDuration = maxOf(taskStats.maximumDuration, duration),
)
}
}
if (Config.isDebug) {
Log.v(Config.logTag, "Persistent repeating task $name got result $result after ${startedAt.elapsedSince()}")
}
}
/**
* Track the result of a task.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
suspend fun trackRepeatingTask(name: String, task: () -> T): T {
val tracker = trackRepeatingTask(name)
val result: T
try {
result = task()
tracker.trackResult("success")
} catch (t: Throwable) {
tracker.trackResult(t::class.java.simpleName)
throw t
}
return result
}
@JvmStatic
@CheckResult
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun getRepeatingTasks() = runBlocking {
repeatingTaskMutex.withLock {
persistentRepeatingTasksMutex.withLock {
repeatingTasks.toMap().mapValues { entry -> entry.value.toMap() } +
persistentRepeatingTasks.toMap().mapValues { entry -> entry.value.toMap() }
}
}
}
@JvmStatic
@CheckResult
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun getTasks() = runBlocking {
taskMutex.withLock { tasks.toMap() }
}
private suspend fun clearAllTasks() = supervisorScope {
val tasksAsync = async { taskMutex.withLock { tasks.clear() } }
val repeatingTasksAsync = async { repeatingTaskMutex.withLock { repeatingTasks.clear() } }
tasksAsync.await()
repeatingTasksAsync.await()
}
}
/**
* Keep track of a single stat's duration and result
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
interface StatTracker {
/**
* When this task was started.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
val startedAt: ClockMark
/**
* Track the result from a stat.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
suspend fun trackResult(result: String? = null)
}
private object StatTrackerNoOpImpl : StatTracker {
override val startedAt = Clock.markNow()
override suspend fun trackResult(result: String?) { /* do nothing */ }
}
private class StatTrackerImpl(private val onComplete: suspend (ClockMark, String?) -> Unit) : StatTracker {
override val startedAt = Clock.markNow()
override suspend fun trackResult(result: String?) = coroutineScope { launch { onComplete(startedAt, result) } }.let { Unit }
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class TaskStats(
val started: ClockMark,
val duration: Duration,
val result: String?,
)
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class RepeatingTaskStats(
val executions: Int,
val startedAt: ClockMark,
val totalDuration: Duration,
val totalCpuDuration: Duration,
val minimumDuration: Duration,
val maximumDuration: Duration,
) {
fun averageDuration() = totalCpuDuration / executions
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/Storage.kt
================================================
package com.getbouncer.scan.framework
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
private const val STORAGE_FILE_NAME = "bouncer_shared_prefs"
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
interface Storage {
/**
* Store a String in app storage by a [key].
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun storeValue(key: String, value: String): Boolean
/**
* Store a Long in app storage by a [key].
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun storeValue(key: String, value: Long): Boolean
/**
* Store an Int in app storage by a [key].
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun storeValue(key: String, value: Int): Boolean
/**
* Store a Float in app storage by a [key].
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun storeValue(key: String, value: Float): Boolean
/**
* Store a Boolean in app storage by a [key].
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun storeValue(key: String, value: Boolean): Boolean
/**
* Retrieve a String from app storage by a [key].
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun getString(key: String, defaultValue: String): String
/**
* Retrieve a Long from app storage by a [key].
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun getLong(key: String, defaultValue: Long): Long
/**
* Retrieve an Int from app storage by a [key].
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun getInt(key: String, defaultValue: Int): Int
/**
* Retrieve a Float from app storage by a [key].
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun getFloat(key: String, defaultValue: Float): Float
/**
* Retrieve a Boolean from app storage by a [key].
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun getBoolean(key: String, defaultValue: Boolean): Boolean
/**
* Clears out a single value from storage.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun remove(key: String): Boolean
/**
* Clear out all values from storage.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun clear(): Boolean
}
/**
* A class that handles access to storage.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
object StorageFactory {
fun getStorageInstance(context: Context, purpose: String): Storage =
SharedPreferencesStorage(context.applicationContext, purpose)
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class SharedPreferencesStorage(private val context: Context, private val purpose: String) : Storage {
private val sharedPrefs: SharedPreferences? by lazy {
context.getSharedPreferences(STORAGE_FILE_NAME, Context.MODE_PRIVATE)
}
override fun storeValue(key: String, value: String) = sharedPrefs?.run {
with(edit()) {
putString("${purpose}_$key", value)
commit()
}
} ?: false.apply {
Log.e(Config.logTag, "Shared preferences is unavailable to store $value for $key")
}
override fun storeValue(key: String, value: Long) = sharedPrefs?.run {
with(edit()) {
putLong("${purpose}_$key", value)
commit()
}
} ?: false.apply {
Log.e(Config.logTag, "Shared preferences is unavailable to store $value for $key")
}
override fun storeValue(key: String, value: Int) = sharedPrefs?.run {
with(edit()) {
putInt("${purpose}_$key", value)
commit()
}
} ?: false.apply {
Log.e(Config.logTag, "Shared preferences is unavailable to store $value for $key")
}
override fun storeValue(key: String, value: Float) = sharedPrefs?.run {
with(edit()) {
putFloat("${purpose}_$key", value)
commit()
}
} ?: false.apply {
Log.e(Config.logTag, "Shared preferences is unavailable to store $value for $key")
}
override fun storeValue(key: String, value: Boolean) = sharedPrefs?.run {
with(edit()) {
putBoolean("${purpose}_$key", value)
commit()
}
} ?: false.apply {
Log.e(Config.logTag, "Shared preferences is unavailable to store $value for $key")
}
override fun getString(key: String, defaultValue: String): String {
return try {
sharedPrefs?.getString("${purpose}_$key", defaultValue) ?: defaultValue.apply {
Log.e(Config.logTag, "Shared preferences is unavailable to retrieve a String for $key")
}
} catch (t: Throwable) {
when (t) {
is ClassCastException -> Log.e(Config.logTag, "Attempted to read String, but $key is not a String", t)
else -> Log.d(Config.logTag, "Error retrieving String for $key", t)
}
defaultValue
}
}
override fun getLong(key: String, defaultValue: Long): Long {
return try {
sharedPrefs?.getLong("${purpose}_$key", defaultValue) ?: defaultValue.apply {
Log.e(Config.logTag, "Shared preferences is unavailable to retrieve a Long for $key")
}
} catch (t: Throwable) {
when (t) {
is ClassCastException -> Log.e(Config.logTag, "Attempted to read Long, but $key is not a Long", t)
else -> Log.d(Config.logTag, "Error retrieving Long for $key", t)
}
defaultValue
}
}
override fun getInt(key: String, defaultValue: Int): Int {
return try {
sharedPrefs?.getInt("${purpose}_$key", defaultValue) ?: defaultValue.apply {
Log.e(Config.logTag, "Shared preferences is unavailable to retrieve an Int for $key")
}
} catch (t: Throwable) {
when (t) {
is ClassCastException -> Log.e(Config.logTag, "Attempted to read Int, but $key is not a Int", t)
else -> Log.d(Config.logTag, "Error retrieving Int for $key", t)
}
defaultValue
}
}
override fun getFloat(key: String, defaultValue: Float): Float {
return try {
sharedPrefs?.getFloat("${purpose}_$key", defaultValue) ?: defaultValue.apply {
Log.e(Config.logTag, "Shared preferences is unavailable to retrieve a Float for $key")
}
} catch (t: Throwable) {
when (t) {
is ClassCastException -> Log.e(Config.logTag, "Attempted to read Float, but $key is not a Float", t)
else -> Log.d(Config.logTag, "Error retrieving Float for $key", t)
}
defaultValue
}
}
override fun getBoolean(key: String, defaultValue: Boolean): Boolean {
return try {
sharedPrefs?.getBoolean("${purpose}_$key", defaultValue) ?: defaultValue.apply {
Log.e(Config.logTag, "Shared preferences is unavailable to retrieve a Boolean for $key")
}
} catch (t: Throwable) {
when (t) {
is ClassCastException -> Log.e(Config.logTag, "Attempted to read Boolean, but $key is not a Boolean", t)
else -> Log.d(Config.logTag, "Error retrieving Boolean for $key", t)
}
defaultValue
}
}
override fun remove(key: String): Boolean = sharedPrefs?.run {
with(edit()) {
remove(key)
commit()
}
} ?: false.apply {
Log.e(Config.logTag, "Shared preferences is unavailable to remove values")
}
override fun clear(): Boolean = sharedPrefs?.run {
with(edit()) {
clear()
commit()
}
} ?: false.apply {
Log.e(Config.logTag, "Shared preferences is unavailable to clear values")
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/TrackedImage.kt
================================================
package com.getbouncer.scan.framework
/**
* An image with a stat tracker.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class TrackedImage(
val image: ImageType,
val tracker: StatTracker,
)
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/api/BouncerApi.kt
================================================
@file:JvmName("BouncerApi")
package com.getbouncer.scan.framework.api
import android.content.Context
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.framework.api.dto.AppInfo
import com.getbouncer.scan.framework.api.dto.BouncerErrorResponse
import com.getbouncer.scan.framework.api.dto.ClientDevice
import com.getbouncer.scan.framework.api.dto.ModelDetailsRequest
import com.getbouncer.scan.framework.api.dto.ModelDetailsResponse
import com.getbouncer.scan.framework.api.dto.ModelSignedUrlResponse
import com.getbouncer.scan.framework.api.dto.ModelVersion
import com.getbouncer.scan.framework.api.dto.ScanStatistics
import com.getbouncer.scan.framework.api.dto.StatsPayload
import com.getbouncer.scan.framework.api.dto.ValidateApiKeyResponse
import com.getbouncer.scan.framework.ml.getLoadedModelVersions
import com.getbouncer.scan.framework.util.AppDetails
import com.getbouncer.scan.framework.util.Device
import com.getbouncer.scan.framework.util.getPlatform
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
private const val STATS_PATH = "/scan_stats"
private const val API_KEY_VALIDATION_PATH = "/v1/api_key/validate"
private const val MODEL_SIGNED_URL_PATH = "/v1/signed_url/model/%s/%s/android/%s"
private const val MODEL_DETAILS_PATH = "/v2/model_details"
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
const val ERROR_CODE_NOT_AUTHENTICATED = "not_authenticated"
/**
* Upload stats data to bouncer servers.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun uploadScanStats(
context: Context,
instanceId: String,
scanId: String?,
device: Device,
appDetails: AppDetails,
scanStatistics: ScanStatistics,
) = GlobalScope.launch(Dispatchers.IO) {
postData(
context = context.applicationContext,
path = STATS_PATH,
data = StatsPayload(
instanceId = instanceId,
scanId = scanId,
device = ClientDevice.fromDevice(device),
app = AppInfo.fromAppDetails(appDetails),
scanStats = scanStatistics,
modelVersions = getLoadedModelVersions().map { ModelVersion.fromModelLoadDetails(it) },
),
requestSerializer = StatsPayload.serializer(),
)
}
/**
* Validate an API key.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
suspend fun validateApiKey(context: Context): NetworkResult =
withContext(Dispatchers.IO) {
getForResult(
context = context,
path = API_KEY_VALIDATION_PATH,
responseSerializer = ValidateApiKeyResponse.serializer(),
errorSerializer = BouncerErrorResponse.serializer(),
)
}
/**
* Get a signed URL for a model.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
suspend fun getModelSignedUrl(
context: Context,
modelClass: String,
modelVersion: String,
modelFileName: String,
): NetworkResult =
withContext(Dispatchers.IO) {
getForResult(
context = context,
path = MODEL_SIGNED_URL_PATH.format(modelClass, modelVersion, modelFileName),
responseSerializer = ModelSignedUrlResponse.serializer(),
errorSerializer = BouncerErrorResponse.serializer(),
)
}
/**
* Get details about a model.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
suspend fun getModelDetails(
context: Context,
modelClass: String,
modelFrameworkVersion: Int,
cachedModelHash: String?,
cachedModelHashAlgorithm: String?,
): NetworkResult =
withContext(Dispatchers.IO) {
postForResult(
context = context,
path = MODEL_DETAILS_PATH,
requestSerializer = ModelDetailsRequest.serializer(),
responseSerializer = ModelDetailsResponse.serializer(),
errorSerializer = BouncerErrorResponse.serializer(),
data = ModelDetailsRequest(
platform = getPlatform(),
modelClass = modelClass,
modelFrameworkVersion = modelFrameworkVersion,
cachedModelHash = cachedModelHash,
cachedModelHashAlgorithm = cachedModelHashAlgorithm,
betaOptIn = Config.betaModelOptIn,
),
)
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/api/Network.kt
================================================
@file:JvmName("Network")
package com.getbouncer.scan.framework.api
import android.content.Context
import android.util.Base64
import android.util.Log
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.framework.NetworkConfig
import com.getbouncer.scan.framework.time.Timer
import com.getbouncer.scan.framework.util.DeviceIds
import com.getbouncer.scan.framework.util.cacheFirstResult
import com.getbouncer.scan.framework.util.getAppPackageName
import com.getbouncer.scan.framework.util.getDeviceName
import com.getbouncer.scan.framework.util.getOsVersion
import com.getbouncer.scan.framework.util.getPlatform
import com.getbouncer.scan.framework.util.getSdkFlavor
import com.getbouncer.scan.framework.util.getSdkVersion
import com.getbouncer.scan.framework.util.retry
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStreamReader
import java.io.OutputStream
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
import java.util.zip.GZIPOutputStream
private const val REQUEST_METHOD_GET = "GET"
private const val REQUEST_METHOD_POST = "POST"
private const val REQUEST_PROPERTY_AUTHENTICATION = "x-bouncer-auth"
private const val REQUEST_PROPERTY_DEVICE_ID = "x-bouncer-device-id"
private const val REQUEST_PROPERTY_USER_AGENT = "User-Agent"
private const val REQUEST_PROPERTY_CONTENT_TYPE = "Content-Type"
private const val REQUEST_PROPERTY_CONTENT_ENCODING = "Content-Encoding"
private const val CONTENT_TYPE_JSON = "application/json; utf-8"
private const val CONTENT_ENCODING_GZIP = "gzip"
/**
* The size of a TCP network packet. If smaller than this, there is no benefit to GZIP.
*/
private const val GZIP_MIN_SIZE_BYTES = 1500
private val networkTimer by lazy { Timer.newInstance(Config.logTag, "network") }
/**
* Send a post request to a bouncer endpoint.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
suspend fun postForResult(
context: Context,
path: String,
data: Request,
requestSerializer: KSerializer,
responseSerializer: KSerializer,
errorSerializer: KSerializer
): NetworkResult =
translateNetworkResult(
networkResult = postJsonWithRetries(
context = context,
path = path,
jsonData = Config.json.encodeToString(requestSerializer, data)
),
responseSerializer = responseSerializer,
errorSerializer = errorSerializer
)
/**
* Send a post request to a bouncer endpoint and ignore the response.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
suspend fun postData(
context: Context,
path: String,
data: Request,
requestSerializer: KSerializer
) {
postJsonWithRetries(
context = context,
path = path,
jsonData = Config.json.encodeToString(requestSerializer, data)
)
}
/**
* Send a get request to a bouncer endpoint and parse the response.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
suspend fun getForResult(
context: Context,
path: String,
responseSerializer: KSerializer,
errorSerializer: KSerializer
): NetworkResult =
translateNetworkResult(getWithRetries(context, path), responseSerializer, errorSerializer)
/**
* Translate a string network result to a response or error.
*/
private fun translateNetworkResult(
networkResult: NetworkResult,
responseSerializer: KSerializer,
errorSerializer: KSerializer
): NetworkResult = when (networkResult) {
is NetworkResult.Success ->
try {
NetworkResult.Success(
responseCode = networkResult.responseCode,
body = Config.json.decodeFromString(responseSerializer, networkResult.body)
)
} catch (t: Throwable) {
try {
NetworkResult.Error(
responseCode = networkResult.responseCode,
error = Config.json.decodeFromString(errorSerializer, networkResult.body)
)
} catch (et: Throwable) {
NetworkResult.Exception(networkResult.responseCode, t)
}
}
is NetworkResult.Error ->
try {
NetworkResult.Error(
responseCode = networkResult.responseCode,
error = Config.json.decodeFromString(errorSerializer, networkResult.error)
)
} catch (t: Throwable) {
NetworkResult.Exception(networkResult.responseCode, t)
}
is NetworkResult.Exception ->
NetworkResult.Exception(
responseCode = networkResult.responseCode,
exception = networkResult.exception
)
}
/**
* Send a post request to a bouncer endpoint with retries.
*/
private suspend fun postJsonWithRetries(
context: Context,
path: String,
jsonData: String
): NetworkResult =
try {
retry(
retryDelay = NetworkConfig.retryDelay,
times = NetworkConfig.retryTotalAttempts
) {
val result = postJson(context, path, jsonData)
if (result.responseCode in NetworkConfig.retryStatusCodes) {
throw RetryNetworkRequestException(result)
} else {
result
}
}
} catch (e: RetryNetworkRequestException) {
e.result
}
/**
* Send a get request to a bouncer endpoint with retries.
*/
private suspend fun getWithRetries(context: Context, path: String): NetworkResult =
try {
retry(
retryDelay = NetworkConfig.retryDelay,
times = NetworkConfig.retryTotalAttempts
) {
val result = get(context, path)
if (result.responseCode in NetworkConfig.retryStatusCodes) {
throw RetryNetworkRequestException(result)
} else {
result
}
}
} catch (e: RetryNetworkRequestException) {
e.result
}
/**
* Send a post request to a bouncer endpoint.
*/
private fun postJson(
context: Context,
path: String,
jsonData: String
): NetworkResult = networkTimer.measure(path) {
val fullPath = if (path.startsWith("/")) path else "/$path"
val url = URL("${getBaseUrl()}$fullPath")
var responseCode = -1
try {
with(url.openConnection() as HttpURLConnection) {
requestMethod = REQUEST_METHOD_POST
// Set the connection to both send and receive data
doOutput = true
doInput = true
// Set headers
setRequestHeaders(context)
setRequestProperty(REQUEST_PROPERTY_CONTENT_TYPE, CONTENT_TYPE_JSON)
// Write the data
if (NetworkConfig.useCompression && jsonData.toByteArray().size >= GZIP_MIN_SIZE_BYTES) {
setRequestProperty(REQUEST_PROPERTY_CONTENT_ENCODING, CONTENT_ENCODING_GZIP)
writeGzipData(
outputStream,
jsonData
)
} else {
writeData(
outputStream,
jsonData
)
}
// Read the response code. This will block until the response has been received.
responseCode = this.responseCode
// Read the response
when (responseCode) {
in 200 until 300 -> NetworkResult.Success(
responseCode,
readResponse(this)
)
else -> NetworkResult.Error(
responseCode,
readResponse(this)
)
}
}
} catch (t: Throwable) {
Log.w(Config.logTag, "Failed network request to endpoint $url", t)
NetworkResult.Exception(responseCode, t)
}
}
/**
* Send a get request to a bouncer endpoint.
*/
private fun get(context: Context, path: String): NetworkResult = networkTimer.measure(path) {
val fullPath = if (path.startsWith("/")) path else "/$path"
val url = URL("${getBaseUrl()}$fullPath")
var responseCode = -1
try {
with(url.openConnection() as HttpURLConnection) {
requestMethod = REQUEST_METHOD_GET
// Set the connection to only receive data
doOutput = false
doInput = true
// Set headers
setRequestHeaders(context)
// Read the response code. This will block until the response has been received.
responseCode = this.responseCode
// Read the response
when (responseCode) {
in 200 until 300 -> NetworkResult.Success(
responseCode,
readResponse(this)
)
else -> NetworkResult.Error(
responseCode,
readResponse(this)
)
}
}
} catch (t: Throwable) {
Log.w(Config.logTag, "Failed network request to endpoint $url", t)
NetworkResult.Exception(responseCode, t)
}
}
@Throws(IOException::class)
suspend fun downloadFileWithRetries(context: Context, url: URL, outputFile: File) = retry(
NetworkConfig.retryDelay,
excluding = listOf(FileNotFoundException::class.java)
) {
downloadFile(context, url, outputFile)
}
/**
* Download a file.
*/
@Throws(IOException::class)
private fun downloadFile(context: Context, url: URL, outputFile: File) = networkTimer.measure(url.toString()) {
try {
with(url.openConnection() as HttpURLConnection) {
requestMethod = REQUEST_METHOD_GET
// Set the connection to only receive data
doOutput = false
doInput = true
// set headers
setRequestHeaders(context)
// Read the response code. This will block until the response has been received.
val responseCode = this.responseCode
inputStream.use { stream ->
FileOutputStream(outputFile).use { stream.copyTo(it) }
}
responseCode
}
} catch (t: Throwable) {
Log.w(Config.logTag, "Failed network request to endpoint $url", t)
throw t
}
}
/**
* Set the required request headers on an HttpURLConnection
*/
private fun HttpURLConnection.setRequestHeaders(context: Context) {
setRequestProperty(REQUEST_PROPERTY_AUTHENTICATION, Config.apiKey)
setRequestProperty(REQUEST_PROPERTY_USER_AGENT, buildUserAgent(context))
setRequestProperty(REQUEST_PROPERTY_DEVICE_ID, buildDeviceId(context))
}
@Serializable
private data class DeviceIdStructure(
/**
* android_id
*/
val a: String,
/**
* vendor_id
*/
val v: String,
/**
* advertising_id
*/
val d: String
)
private val buildDeviceId = cacheFirstResult { context: Context ->
DeviceIds.fromContext(context).run {
Base64.encodeToString(
Config.json.encodeToString(
DeviceIdStructure.serializer(),
DeviceIdStructure(a = androidId ?: "", v = "", d = "")
).toByteArray(Charsets.UTF_8),
Base64.URL_SAFE
)
}
}
private val buildUserAgent = cacheFirstResult { context: Context ->
"bouncer/${getPlatform()}/${getAppPackageName(context)}/${getDeviceName()}/${getOsVersion()}/${getSdkVersion()}/${getSdkFlavor()}"
}
private fun writeGzipData(outputStream: OutputStream, data: String) {
OutputStreamWriter(
GZIPOutputStream(
outputStream
)
).use {
it.write(data)
it.flush()
}
}
private fun writeData(outputStream: OutputStream, data: String) {
OutputStreamWriter(outputStream).use {
it.write(data)
it.flush()
}
}
private fun readResponse(connection: HttpURLConnection): String =
InputStreamReader(connection.inputStream).use {
it.readLines().joinToString(separator = "\n")
}
/**
* Get the [NetworkConfig.baseUrl] with no trailing slashes.
*/
private fun getBaseUrl() = if (NetworkConfig.baseUrl.endsWith("/")) {
NetworkConfig.baseUrl.substring(0, NetworkConfig.baseUrl.length - 1)
} else {
NetworkConfig.baseUrl
}
/**
* An exception that should never be thrown, but is required for typing.
*/
private class RetryNetworkRequestException(val result: NetworkResult) : Exception()
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/api/NetworkResult.kt
================================================
package com.getbouncer.scan.framework.api
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
sealed class NetworkResult(open val responseCode: Int) {
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class Success(override val responseCode: Int, val body: Success) : NetworkResult(responseCode)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class Error(override val responseCode: Int, val error: Error) : NetworkResult(responseCode)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class Exception(override val responseCode: Int, val exception: Throwable) : NetworkResult(responseCode)
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/AppInfo.kt
================================================
package com.getbouncer.scan.framework.api.dto
import androidx.annotation.RestrictTo
import com.getbouncer.scan.framework.util.AppDetails
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class AppInfo(
@SerialName("app_package_name") val appPackageName: String?,
@SerialName("application_id") val applicationId: String,
@SerialName("library_package_name") val libraryPackageName: String,
@SerialName("sdk_version") val sdkVersion: String,
@SerialName("sdk_version_code") val sdkVersionCode: Int,
@SerialName("sdk_flavor") val sdkFlavor: String,
@SerialName("is_debug_build") val isDebugBuild: Boolean
) {
companion object {
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun fromAppDetails(appDetails: AppDetails): AppInfo = AppInfo(
appPackageName = appDetails.appPackageName,
applicationId = appDetails.applicationId,
libraryPackageName = appDetails.libraryPackageName,
sdkVersion = appDetails.sdkVersion,
sdkVersionCode = appDetails.sdkVersionCode,
sdkFlavor = appDetails.sdkFlavor,
isDebugBuild = appDetails.isDebugBuild
)
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/BouncerErrorResponse.kt
================================================
package com.getbouncer.scan.framework.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class BouncerErrorResponse(
@SerialName("status") val status: String,
@SerialName("error_code") val errorCode: String,
@SerialName("error_message") val errorMessage: String,
@SerialName("error_payload") val errorPayload: String?
)
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/ClientDevice.kt
================================================
package com.getbouncer.scan.framework.api.dto
import androidx.annotation.RestrictTo
import com.getbouncer.scan.framework.util.Device
import com.getbouncer.scan.framework.util.DeviceIds
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class ClientDevice(
@SerialName("ids") val ids: ClientDeviceIds,
@SerialName("type") val name: String,
@SerialName("boot_count") val bootCount: Int,
@SerialName("locale") val locale: String?,
@SerialName("carrier") val carrier: String?,
@SerialName("network_operator") val networkOperator: String?,
@SerialName("phone_type") val phoneType: Int?,
@SerialName("phone_count") val phoneCount: Int,
@SerialName("os_version") val osVersion: String,
@SerialName("platform") val platform: String
) {
companion object {
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun fromDevice(device: Device) = ClientDevice(
ids = ClientDeviceIds.fromDeviceIds(device.ids),
name = device.name,
bootCount = device.bootCount,
locale = device.locale,
carrier = device.carrier,
networkOperator = device.networkOperator,
phoneType = device.phoneType,
phoneCount = device.phoneCount,
osVersion = device.osVersion.toString(),
platform = device.platform
)
}
}
@Serializable
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class ClientDeviceIds(
@SerialName("vendor_id") val androidId: String?
) {
companion object {
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun fromDeviceIds(deviceIds: DeviceIds) = ClientDeviceIds(
androidId = deviceIds.androidId
)
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/ClientStats.kt
================================================
package com.getbouncer.scan.framework.api.dto
import com.getbouncer.scan.framework.RepeatingTaskStats
import com.getbouncer.scan.framework.Stats
import com.getbouncer.scan.framework.TaskStats
import com.getbouncer.scan.framework.ml.ModelLoadDetails
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class StatsPayload(
@SerialName("instance_id") val instanceId: String,
@SerialName("scan_id") val scanId: String?,
@SerialName("payload_version") val payloadVersion: Int = 2,
@SerialName("device") val device: ClientDevice,
@SerialName("app") val app: AppInfo,
@SerialName("scan_stats") val scanStats: ScanStatistics,
@SerialName("model_versions") val modelVersions: List,
)
@Serializable
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class ScanStatistics(
@SerialName("tasks") val tasks: Map>,
@SerialName("repeating_tasks") val repeatingTasks: Map>
) {
companion object {
@JvmStatic
fun fromStats() = ScanStatistics(
tasks = Stats.getTasks().mapValues { entry ->
entry.value.map { TaskStatistics.fromTaskStats(it) }
},
repeatingTasks = Stats.getRepeatingTasks().mapValues { repeatingTasks ->
repeatingTasks.value.map { resultMap ->
RepeatingTaskStatistics.fromRepeatingTaskStats(
result = resultMap.key,
repeatingTaskStats = resultMap.value,
)
}
}
)
}
}
@Serializable
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class ModelVersion(
@SerialName("name") val name: String,
@SerialName("version") val version: String,
@SerialName("framework_version") val frameworkVersion: Int,
@SerialName("loaded_successfully") val loadedSuccessfully: Boolean
) {
companion object {
fun fromModelLoadDetails(details: ModelLoadDetails) = ModelVersion(
name = details.modelClass,
version = details.modelVersion,
frameworkVersion = details.modelFrameworkVersion,
loadedSuccessfully = details.success
)
}
}
@Serializable
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class TaskStatistics(
@SerialName("started_at_ms") val startedAtMs: Long,
@SerialName("duration_ms") val durationMs: Long,
@SerialName("result") val result: String?
) {
companion object {
@JvmStatic
fun fromTaskStats(taskStats: TaskStats) = TaskStatistics(
startedAtMs = taskStats.started.toMillisecondsSinceEpoch(),
durationMs = taskStats.duration.inMilliseconds.toLong(),
result = taskStats.result
)
}
}
@Serializable
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class RepeatingTaskStatistics(
@SerialName("result") val result: String,
@SerialName("executions") val executions: Int,
@SerialName("start_time_ms") val startTimeMs: Long,
@SerialName("total_duration_ms") val totalDurationMs: Long,
@SerialName("total_cpu_duration_ms") val totalCpuDurationMs: Long,
@SerialName("average_duration_ms") val averageDurationMs: Long,
@SerialName("minimum_duration_ms") val minimumDurationMs: Long,
@SerialName("maximum_duration_ms") val maximumDurationMs: Long,
) {
companion object {
@JvmStatic
fun fromRepeatingTaskStats(result: String, repeatingTaskStats: RepeatingTaskStats) = RepeatingTaskStatistics(
result = result,
executions = repeatingTaskStats.executions,
startTimeMs = repeatingTaskStats.startedAt.toMillisecondsSinceEpoch(),
totalDurationMs = repeatingTaskStats.totalDuration.inMilliseconds.toLong(),
totalCpuDurationMs = repeatingTaskStats.totalCpuDuration.inMilliseconds.toLong(),
averageDurationMs = repeatingTaskStats.averageDuration().inMilliseconds.toLong(),
minimumDurationMs = repeatingTaskStats.minimumDuration.inMilliseconds.toLong(),
maximumDurationMs = repeatingTaskStats.maximumDuration.inMilliseconds.toLong(),
)
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/ModelDetails.kt
================================================
package com.getbouncer.scan.framework.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class ModelDetailsRequest(
@SerialName("platform") val platform: String,
@SerialName("model_class") val modelClass: String,
@SerialName("model_framework_version") val modelFrameworkVersion: Int,
@SerialName("cached_model_hash") val cachedModelHash: String?,
@SerialName("cached_model_hash_algorithm") val cachedModelHashAlgorithm: String?,
@SerialName("beta_opt_in") val betaOptIn: Boolean?,
)
@Serializable
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class ModelDetailsResponse(
@SerialName("model_url") val url: String?,
@SerialName("model_version") val modelVersion: String,
@SerialName("model_hash") val hash: String,
@SerialName("model_hash_algorithm") val hashAlgorithm: String,
@SerialName("query_again_after_ms") val queryAgainAfterMs: Long? = 0,
)
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/ModelSignedUrlResponse.kt
================================================
package com.getbouncer.scan.framework.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class ModelSignedUrlResponse(
@SerialName("model_url") val modelUrl: String
)
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/ValidateApiKeyResponse.kt
================================================
package com.getbouncer.scan.framework.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class ValidateApiKeyResponse(
@SerialName("is_valid_api_key") val isApiKeyValid: Boolean,
@SerialName("invalid_key_reason") val keyInvalidReason: String?
)
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/exception/ImageTypeNotSupportedException.kt
================================================
package com.getbouncer.scan.framework.exception
import java.lang.Exception
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
class ImageTypeNotSupportedException(val imageType: Int) : Exception()
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/exception/InvalidBouncerApiKeyException.kt
================================================
package com.getbouncer.scan.framework.exception
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
object InvalidBouncerApiKeyException : Exception()
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/image/BitmapExtensions.kt
================================================
package com.getbouncer.scan.framework.image
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Rect
import android.util.Size
import androidx.annotation.CheckResult
import com.getbouncer.scan.framework.util.centerOn
import com.getbouncer.scan.framework.util.intersectionWith
import com.getbouncer.scan.framework.util.move
import com.getbouncer.scan.framework.util.resizeRegion
import com.getbouncer.scan.framework.util.size
import com.getbouncer.scan.framework.util.toRect
import kotlin.math.max
import kotlin.math.min
/**
* Crop a [Bitmap] to a given [Rect]. The crop must have a positive area and must be contained within the bounds of the
* source [Bitmap].
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Bitmap.crop(crop: Rect): Bitmap {
require(crop.left < crop.right && crop.top < crop.bottom) { "Cannot use negative crop" }
require(crop.left >= 0 && crop.top >= 0 && crop.bottom <= this.height && crop.right <= this.width) {
"Crop is larger than source image"
}
return Bitmap.createBitmap(this, crop.left, crop.top, crop.width(), crop.height())
}
/**
* Rotate a [Bitmap] by the given [rotationDegrees].
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Bitmap.rotate(rotationDegrees: Float): Bitmap = if (rotationDegrees != 0F) {
val matrix = Matrix()
matrix.postRotate(rotationDegrees)
Bitmap.createBitmap(this, 0, 0, this.width, this.height, matrix, true)
} else {
this
}
/**
* Scale a [Bitmap] by a given [percentage].
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Bitmap.scale(percentage: Float, filter: Boolean = false): Bitmap = if (percentage == 1F) {
this
} else {
Bitmap.createScaledBitmap(
this,
(width * percentage).toInt(),
(height * percentage).toInt(),
filter
)
}
/**
* Get the size of a [Bitmap].
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Bitmap.size() = Size(this.width, this.height)
/**
* Scale the [Bitmap] to circumscribe the given [Size], then crop the excess.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Bitmap.scaleAndCrop(size: Size, filter: Boolean = false): Bitmap =
if (size.width == width && size.height == height) {
this
} else {
val scaleFactor = max(size.width.toFloat() / this.width, size.height.toFloat() / this.height)
val scaled = this.scale(scaleFactor, filter)
scaled.crop(size.centerOn(scaled.size().toRect()))
}
/**
* Crops and image using originalImageRect and places it on finalImageRect, which is filled with
* gray for the best results
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Bitmap.cropWithFill(cropRegion: Rect): Bitmap {
val intersectionRegion = this.size().toRect().intersectionWith(cropRegion)
val result = Bitmap.createBitmap(cropRegion.width(), cropRegion.height(), this.config)
val canvas = Canvas(result)
canvas.drawColor(Color.GRAY)
val croppedImage = this.crop(intersectionRegion)
canvas.drawBitmap(
croppedImage,
croppedImage.size().toRect(),
intersectionRegion.move(-cropRegion.left, -cropRegion.top),
null
)
return result
}
/**
* Fragments the [Bitmap] into multiple segments and places them in new segments.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Bitmap.rearrangeBySegments(
segmentMap: Map
): Bitmap {
if (segmentMap.isEmpty()) {
return Bitmap.createBitmap(0, 0, this.config)
}
val newImageDimensions = segmentMap.values.reduce { a, b ->
Rect(
min(a.left, b.left),
min(a.top, b.top),
max(a.right, b.right),
max(a.bottom, b.bottom)
)
}
val newImageSize = newImageDimensions.size()
val result = Bitmap.createBitmap(newImageSize.width, newImageSize.height, this.config)
val canvas = Canvas(result)
// This should be using segmentMap.forEach, but doing so seems to require API 24. It's unclear why this won't use
// the kotlin.collections version of `forEach`, but it's not during compile.
for (it in segmentMap) {
val from = it.key
val to = it.value.move(-newImageDimensions.left, -newImageDimensions.top)
val segment = this.crop(from).scale(to.size())
canvas.drawBitmap(
segment,
to.left.toFloat(),
to.top.toFloat(),
null
)
}
return result
}
/**
* Selects a region from the source [Bitmap], resizing that to a new region, and transforms the remainder of the
* [Bitmap] into a border. See [resizeRegion] and [rearrangeBySegments].
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Bitmap.zoom(
originalRegion: Rect,
newRegion: Rect,
newImageSize: Size
): Bitmap {
// Produces a map of rects to rects which are used to map segments of the old image onto the new one
val regionMap = this.size().resizeRegion(originalRegion, newRegion, newImageSize)
// construct the bitmap from the region map
return this.rearrangeBySegments(regionMap)
}
fun Bitmap.scale(size: Size, filter: Boolean = false): Bitmap =
if (size.width == width && size.height == height) {
this
} else {
Bitmap.createScaledBitmap(this, size.width, size.height, filter)
}
/**
* Convert a [Bitmap] to an [MLImage] for use in ML models.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Bitmap.toMLImage(mean: Float = 0F, std: Float = 255F) = MLImage(this, mean, std)
/**
* Convert a [Bitmap] to an [MLImage] for use in ML models.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Bitmap.toMLImage(mean: ImageTransformValues, std: ImageTransformValues) = MLImage(this, mean, std)
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/image/ImageExtensions.kt
================================================
package com.getbouncer.scan.framework.image
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageFormat
import android.graphics.Rect
import android.media.Image
import android.renderscript.RenderScript
import androidx.annotation.CheckResult
import com.getbouncer.scan.framework.exception.ImageTypeNotSupportedException
/**
* Determine if this application supports an image format.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Image.isSupportedFormat() = isSupportedFormat(this.format)
/**
* Determine if this application supports an image format.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun isSupportedFormat(imageFormat: Int) = when (imageFormat) {
ImageFormat.YUV_420_888, ImageFormat.JPEG -> true
ImageFormat.NV21 -> false // this fails on devices with android API 21.
else -> false
}
/**
* Convert an image to a bitmap for processing. This will throw an [ImageTypeNotSupportedException]
* if the image type is not supported (see [isSupportedFormat]).
*/
@CheckResult
@Throws(ImageTypeNotSupportedException::class)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Image.toBitmap(
renderScript: RenderScript,
crop: Rect = Rect(
0,
0,
this.width,
this.height
),
): Bitmap = when (this.format) {
ImageFormat.NV21 -> NV21Image(this).crop(crop).toBitmap(renderScript)
ImageFormat.YUV_420_888 -> NV21Image(this).crop(crop).toBitmap(renderScript)
ImageFormat.JPEG -> jpegToBitmap().crop(crop)
else -> throw ImageTypeNotSupportedException(this.format)
}
@CheckResult
private fun Image.jpegToBitmap(): Bitmap {
require(format == ImageFormat.JPEG) { "Image is not in JPEG format" }
val imageBuffer = planes[0].buffer
val imageBytes = ByteArray(imageBuffer.remaining())
imageBuffer.get(imageBytes)
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/image/MLImage.kt
================================================
package com.getbouncer.scan.framework.image
import android.graphics.Bitmap
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.IntBuffer
import kotlin.math.roundToInt
private const val DIM_PIXEL_SIZE = 3
private const val NUM_BYTES_PER_CHANNEL = 4 // Float.size / Byte.size
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class ImageTransformValues(val red: Float, val green: Float, val blue: Float)
/**
* An image in the required ML input format (array of floats, 3 floats per pixel in R, G, B format).
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
class MLImage(val width: Int, val height: Int, private val imageData: ByteBuffer) {
constructor(bitmap: Bitmap, mean: Float = 0F, std: Float = 255F) : this(
bitmap,
ImageTransformValues(mean, mean, mean),
ImageTransformValues(std, std, std),
)
constructor(bitmap: Bitmap, mean: ImageTransformValues, std: ImageTransformValues) : this(
bitmap.width,
bitmap.height,
IntArray(bitmap.width * bitmap.height)
.also { bitmap.getPixels(it, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) }
.let {
val rgbFloat =
ByteBuffer.allocateDirect(bitmap.width * bitmap.height * DIM_PIXEL_SIZE * NUM_BYTES_PER_CHANNEL)
rgbFloat.order(ByteOrder.nativeOrder())
it.forEach {
// ignore the alpha value ((it shr 24 and 0xFF) - mean.alpha) / std.alpha)
rgbFloat.putFloat(((it shr 16 and 0xFF) - mean.red) / std.red)
rgbFloat.putFloat(((it shr 8 and 0xFF) - mean.green) / std.green)
rgbFloat.putFloat(((it and 0xFF) - mean.blue) / std.blue)
}
rgbFloat
}
)
/**
* Convert an [MLImage] to a [Bitmap]. This is primarily used in testing.
*/
fun toBitmap(mean: Float = 0F, std: Float = 255F) = toBitmap(
ImageTransformValues(mean, mean, mean),
ImageTransformValues(std, std, std),
)
/**
* Convert an [MLImage] to a [Bitmap]. This is primarily used in testing.
*/
fun toBitmap(mean: ImageTransformValues, std: ImageTransformValues): Bitmap {
imageData.rewind()
check(imageData.limit() == width * height) { "ByteBuffer limit does not match expected size" }
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val rgba = IntBuffer.allocate(width * height)
while (this.imageData.hasRemaining()) {
rgba.put(
(0xFF shl 24) + // set 0xFF for the alpha value
(((this.imageData.float * std.red) + mean.red).roundToInt() shl 16) +
(((this.imageData.float * std.green) + mean.green).roundToInt() shl 8) +
(((this.imageData.float * std.blue) + mean.blue).roundToInt())
)
}
rgba.rewind()
bitmap.copyPixelsFromBuffer(rgba)
return bitmap
}
/**
* Get the RBG direct [ByteBuffer] for use in ML models.
*/
fun getData(): ByteBuffer = imageData.rewind() as ByteBuffer
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/image/NV21Image.kt
================================================
package com.getbouncer.scan.framework.image
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageFormat
import android.graphics.Rect
import android.graphics.YuvImage
import android.media.Image
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicYuvToRGB
import android.renderscript.Type
import android.util.Log
import android.util.Size
import androidx.annotation.CheckResult
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.framework.exception.ImageTypeNotSupportedException
import com.getbouncer.scan.framework.util.cacheFirstResult
import com.getbouncer.scan.framework.util.mapArray
import com.getbouncer.scan.framework.util.mapToIntArray
import com.getbouncer.scan.framework.util.toByteArray
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ReadOnlyBufferException
import kotlin.experimental.inv
/**
* Get the RenderScript instance.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val getRenderScript = cacheFirstResult { context: Context -> RenderScript.create(context) }
/**
* An image made of data in the NV21 format.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
class NV21Image(val width: Int, val height: Int, val nv21Data: ByteArray) {
@Throws(ImageTypeNotSupportedException::class)
constructor(image: Image) : this(
image.width,
image.height,
when (image.format) {
ImageFormat.NV21 -> image.planes[0].buffer.toByteArray()
ImageFormat.YUV_420_888 -> image.yuvToNV21Bytes()
else -> throw ImageTypeNotSupportedException(image.format)
}
)
@Throws(ImageTypeNotSupportedException::class)
constructor(yuvImage: YuvImage) : this(
yuvImage.width,
yuvImage.height,
when (yuvImage.yuvFormat) {
ImageFormat.NV21 -> yuvImage.yuvData
else -> throw ImageTypeNotSupportedException(yuvImage.yuvFormat)
}
)
/**
* The size of the [NV21Image].
*/
val size = Size(width, height)
/**
* Crop a region of the [NV21Image].
*
* https://www.programmersought.com/article/75461140907/
*/
fun crop(rect: Rect) = crop(rect.left, rect.top, rect.right, rect.bottom)
/**
* Crop a region of the [NV21Image].
*
* https://www.programmersought.com/article/75461140907/
*/
fun crop(left: Int, top: Int, right: Int, bottom: Int): NV21Image {
if (left > width || top > height) {
return NV21Image(0, 0, ByteArray(0))
}
if (left == 0 && top == 0 && right == width && bottom == height) {
return this
}
// Take the couple
val x = left * 2 / 2
val y = top * 2 / 2
val w = (right - left) * 2 / 2
val h = (bottom - top) * 2 / 2
val yUnit = w * h
val uv = yUnit / 2
val nData = ByteArray(yUnit + uv)
val uvIndexDst = w * h - y / 2 * w
val uvIndexSrc = width * height + x
var srcPos0 = y * width
var destPos0 = 0
var uvSrcPos0 = uvIndexSrc
var uvDestPos0 = uvIndexDst
for (i in y until y + h) {
System.arraycopy(nv21Data, srcPos0 + x, nData, destPos0, w) // y memory block copy
srcPos0 += width
destPos0 += w
if ((i and 1) == 0) {
System.arraycopy(nv21Data, uvSrcPos0, nData, uvDestPos0, w) // uv memory block copy
uvSrcPos0 += width
uvDestPos0 += w
}
}
return NV21Image(w, h, nData)
}
/**
* Rotate the NV21 image an increment of 90 degrees.
*/
fun rotate(rotationDegrees: Int): NV21Image {
require(rotationDegrees % 90 == 0) { "Can only rotate increments of 90 degrees" }
val rotation = if (rotationDegrees % 360 < 0) rotationDegrees % 360 + 360 else rotationDegrees % 360
if (rotation == 0) return this
val output = ByteArray(nv21Data.size)
val frameSize = width * height
val swap = rotation % 180 != 0
val xFlip = rotation % 270 != 0
val yFlip = rotation >= 180
for (j in 0 until height) {
for (i in 0 until width) {
val yIn = j * width + i
val uIn = frameSize + (j shr 1) * width + (i and 1.inv())
val vIn = uIn + 1
val wOut = if (swap) height else width
val hOut = if (swap) width else height
val iSwapped = if (swap) j else i
val jSwapped = if (swap) i else j
val iOut = if (xFlip) wOut - iSwapped - 1 else iSwapped
val jOut = if (yFlip) hOut - jSwapped - 1 else jSwapped
val yOut = jOut * wOut + iOut
val uOut = frameSize + (jOut shr 1) * wOut + (iOut and 1.inv())
val vOut = uOut + 1
output[yOut] = (0xff and nv21Data[yIn].toInt()).toByte()
output[uOut] = (0xff and nv21Data[uIn].toInt()).toByte()
output[vOut] = (0xff and nv21Data[vIn].toInt()).toByte()
}
}
return if (rotation == 270 || rotation == 90) {
NV21Image(height, width, output)
} else {
NV21Image(width, height, output)
}
}
// fun scale(percent: Float) {
// TODO("Implement this")
// }
// fun toRGBByteBuffer(mean: ImageTransformValues, std: ImageTransformValues) {
// TODO("Finish implementing this")
// val startTime = System.currentTimeMillis()
// val frameSize = width * height
//
// var yp = 0
//
// val rgba = IntArray(width * height)
//
// for (j in 0 until height) {
// var uvp = frameSize + (j shr 1) * width
// var u = 0
// var v = 0
// for (i in 0 until width) {
// var y = (0xff and data[yp].toInt()) - 16
// if (y < 0) y = 0
// if (i and 1 == 0) {
// v = (0xff and data[uvp++].toInt()) - 128
// u = (0xff and data[uvp++].toInt()) - 128
// }
// val y1192 = 1192 * y
// var r = y1192 + 1634 * v
// var g = y1192 - 833 * v - 400 * u
// var b = y1192 + 2066 * u
// if (r < 0) r = 0 else if (r > 262143) r = 262143
// if (g < 0) g = 0 else if (g > 262143) g = 262143
// if (b < 0) b = 0 else if (b > 262143) b = 262143
//
// // rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) &
// // 0xff00) | ((b >> 10) & 0xff);
// // rgba, divide 2^10 ( >> 10)
// rgba[yp] = (r shl 14 and -0x1000000 or (g shl 6 and 0xff0000)
// or (b shr 2 or 0xff00))
// yp++
// }
// }
//
// val rgbFloat =
// ByteBuffer.allocateDirect(this.width * this.height * DIM_PIXEL_SIZE * NUM_BYTES_PER_CHANNEL)
// rgbFloat.order(ByteOrder.nativeOrder())
//
// rgba.forEach {
// rgbFloat.putFloat(((it shr 24 and 0xFF) - mean.red) / std.red)
// rgbFloat.putFloat(((it shr 16 and 0xFF) - mean.green) / std.green)
// rgbFloat.putFloat(((it shr 8 and 0xFF) - mean.blue) / std.blue)
// // ignore the alpha value ((it and 0xFF) - mean.alpha) / std.alpha)
// }
//
// rgbFloat.rewind()
// Log.d(Config.logTag, "Bitmap to RGB Byte buffer conversion took: ${System.currentTimeMillis() - startTime} ms")
// return rgbFloat
// }
/**
* Convert to a [YuvImage].
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun toYuvImage() = YuvImage(
nv21Data,
ImageFormat.NV21,
width,
height,
null
)
/**
* https://github.com/silvaren/easyrs/blob/c8eed0f0b713bbb1eb375aca23d615677e8adb3c/easyrs/src/main/java/io/github/silvaren/easyrs/tools/YuvToRgb.java
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun toBitmap(renderScript: RenderScript): Bitmap {
val yuvTypeBuilder: Type.Builder = Type.Builder(renderScript, Element.U8(renderScript)).setX(nv21Data.size)
val yuvType: Type = yuvTypeBuilder.create()
val yuvAllocation = Allocation.createTyped(renderScript, yuvType, Allocation.USAGE_SCRIPT)
yuvAllocation.copyFrom(nv21Data)
val rgbTypeBuilder: Type.Builder = Type.Builder(renderScript, Element.RGBA_8888(renderScript))
rgbTypeBuilder.setX(width)
rgbTypeBuilder.setY(height)
val rgbAllocation = Allocation.createTyped(renderScript, rgbTypeBuilder.create())
val yuvToRgbScript = ScriptIntrinsicYuvToRGB.create(renderScript, Element.RGBA_8888(renderScript))
yuvToRgbScript.setInput(yuvAllocation)
yuvToRgbScript.forEach(rgbAllocation)
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
rgbAllocation.copyTo(bitmap)
// remove allocated objects
yuvType.destroy()
yuvAllocation.destroy()
rgbAllocation.destroy()
yuvToRgbScript.destroy()
return bitmap
}
}
/**
* Convert YUV420_888 image into NV21
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
private fun Image.yuvToNV21Bytes() = yuvPlanesToNV21Fast(
width = width,
height = height,
planeBuffers = planes.mapArray { it.buffer },
rowStrides = planes.mapToIntArray { it.rowStride },
pixelStrides = planes.mapToIntArray { it.pixelStride },
)
/**
* https://stackoverflow.com/questions/32276522/convert-nv21-byte-array-into-bitmap-readable-format
*
* https://stackoverflow.com/questions/41773621/camera2-output-to-bitmap
*
* On Revvl2, average performance is ~27ms
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun yuvPlanesToNV21Compat(
width: Int,
height: Int,
planeBuffers: Array,
rowStrides: IntArray,
pixelStrides: IntArray,
format: Int,
crop: Rect = Rect(0, 0, width, height),
): ByteArray {
val cropWidth = crop.width()
val cropHeight = crop.height()
val nv21Bytes = ByteArray(cropWidth * cropHeight * ImageFormat.getBitsPerPixel(format) / 8)
val rowData = ByteArray(rowStrides[0])
var channelOffset = 0
var outputStride = 1
for (i in planeBuffers.indices) {
when (i) {
0 -> {
channelOffset = 0
outputStride = 1
}
1 -> {
channelOffset = cropWidth * cropHeight + 1
outputStride = 2
}
2 -> {
channelOffset = cropWidth * cropHeight
outputStride = 2
}
}
val buffer = planeBuffers[i]
val rowStride = rowStrides[i]
val pixelStride = pixelStrides[i]
val shift = if (i == 0) 0 else 1
val w = cropWidth shr shift
val h = cropHeight shr shift
buffer.position(rowStride * (crop.top shr shift) + pixelStride * (crop.left shr shift))
for (row in 0 until h) {
var length: Int
if (pixelStride == 1 && outputStride == 1) {
length = w
buffer.get(nv21Bytes, channelOffset, length)
channelOffset += length
} else {
length = (w - 1) * pixelStride + 1
buffer.get(rowData, 0, length)
for (col in 0 until w) {
nv21Bytes[channelOffset] = rowData[col * pixelStride]
channelOffset += outputStride
}
}
if (row < h - 1) {
buffer.position(buffer.position() + rowStride - length)
}
}
}
return nv21Bytes
}
/**
* https://stackoverflow.com/questions/44994510/how-to-convert-rotate-raw-nv21-array-image-android-media-image-from-front-ca
*
* On Revvl2, average performance is ~60ms
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun yuvPlanesToNV21Slow(planeBuffers: Array): ByteArray {
val rez: ByteArray
val buffer0 = planeBuffers[0]
val buffer1 = planeBuffers[1]
val buffer2 = planeBuffers[2]
// actually here should be something like each second byte
// however I simply get the last byte of buffer 2 and the entire buffer 1
val buffer0Size = buffer0.remaining()
val buffer1Size = buffer1.remaining() // / 2 + 1;
val buffer2Size = 1 // buffer2.remaining(); // / 2 + 1;
val buffer0Byte = ByteArray(buffer0Size)
val buffer1Byte = ByteArray(buffer1Size)
val buffer2Byte = ByteArray(buffer2Size)
buffer0[buffer0Byte, 0, buffer0Size]
buffer1[buffer1Byte, 0, buffer1Size]
buffer2[buffer2Byte, buffer2Size - 1, buffer2Size]
val outputStream = ByteArrayOutputStream()
try {
// swap 1 and 2 as blue and red colors are swapped
outputStream.write(buffer0Byte)
outputStream.write(buffer2Byte)
outputStream.write(buffer1Byte)
} catch (e: IOException) {
Log.e(Config.logTag, "Error converting image from YUV to NV21")
}
rez = outputStream.toByteArray()
return rez
}
/**
* Utility function for converting YUV planes into an NV21 byte array
*
* https://stackoverflow.com/questions/52726002/camera2-captured-picture-conversion-from-yuv-420-888-to-nv21/52740776#52740776
*
* On Revvl2, average performance is ~5ms
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun yuvPlanesToNV21Fast(
width: Int,
height: Int,
planeBuffers: Array,
rowStrides: IntArray,
pixelStrides: IntArray,
): ByteArray {
val ySize = width * height
val uvSize = width * height / 4
val nv21 = ByteArray(ySize + uvSize * 2)
val yBuffer = planeBuffers[0] // Y
val uBuffer = planeBuffers[1] // U
val vBuffer = planeBuffers[2] // V
var rowStride = rowStrides[0]
check(pixelStrides[0] == 1)
var pos = 0
if (rowStride == width) { // likely
yBuffer[nv21, 0, ySize]
pos += ySize
} else {
var yBufferPos = -rowStride.toLong() // not an actual position
while (pos < ySize) {
yBufferPos += rowStride.toLong()
yBuffer.position(yBufferPos.toInt())
yBuffer[nv21, pos, width]
pos += width
}
}
rowStride = rowStrides[2]
val pixelStride = pixelStrides[2]
check(rowStride == rowStrides[1])
check(pixelStride == pixelStrides[1])
if (pixelStride == 2 && rowStride == width && uBuffer[0] == vBuffer[1]) {
// maybe V an U planes overlap as per NV21, which means vBuffer[1] is alias of uBuffer[0]
val savePixel = vBuffer[1]
try {
vBuffer.put(1, savePixel.inv())
if (uBuffer[0] == savePixel.inv()) {
vBuffer.put(1, savePixel)
vBuffer.position(0)
uBuffer.position(0)
vBuffer[nv21, ySize, 1]
uBuffer[nv21, ySize + 1, uBuffer.remaining()]
return nv21 // shortcut
}
} catch (ex: ReadOnlyBufferException) {
// unfortunately, we cannot check if vBuffer and uBuffer overlap
}
// unfortunately, the check failed. We must save U and V pixel by pixel
vBuffer.put(1, savePixel)
}
// other optimizations could check if (pixelStride == 1) or (pixelStride == 2),
// but performance gain would be less significant
for (row in 0 until height / 2) {
for (col in 0 until width / 2) {
val vuPos = col * pixelStride + row * rowStride
nv21[pos++] = vBuffer[vuPos]
nv21[pos++] = uBuffer[vuPos]
}
}
return nv21
}
/**
* https://stackoverflow.com/questions/33542708/camera2-api-convert-yuv420-to-rgb-green-out
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun yuvPlanesToBitmap(
width: Int,
height: Int,
planeBuffers: Array,
): Bitmap {
val bitmap = ByteArrayOutputStream()
YuvImage(planeBuffers.toList().toByteArray(), ImageFormat.NV21, width, height, null).compressToJpeg(Rect(0, 0, width, height), 95, bitmap)
return BitmapFactory.decodeByteArray(bitmap.toByteArray(), 0, bitmap.size())
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/image/YuvImageExtensions.kt
================================================
package com.getbouncer.scan.framework.image
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Rect
import android.graphics.YuvImage
import androidx.annotation.CheckResult
import java.io.ByteArrayOutputStream
/**
* Convert a [YuvImage] to a [Bitmap]. This is not an efficient method since it uses an intermediate JPEG compression
* and should be avoided if possible.
*/
@CheckResult
@Deprecated("This method is inefficient and should be avoided if possible")
fun YuvImage.toBitmap(
crop: Rect = Rect(
0,
0,
this.width,
this.height
),
quality: Int = 75
): Bitmap {
val out = ByteArrayOutputStream()
compressToJpeg(crop, quality, out)
val imageBytes = out.toByteArray()
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/interop/BlockingAnalyzer.kt
================================================
package com.getbouncer.scan.framework.interop
import com.getbouncer.scan.framework.Analyzer
import com.getbouncer.scan.framework.AnalyzerFactory
import com.getbouncer.scan.framework.AnalyzerPool
import com.getbouncer.scan.framework.DEFAULT_ANALYZER_PARALLEL_COUNT
import kotlinx.coroutines.runBlocking
/**
* An implementation of an analyzer that does not use suspending functions. This allows interoperability with java.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
abstract class BlockingAnalyzer : Analyzer {
override suspend fun analyze(data: Input, state: State): Output = analyzeBlocking(data, state)
abstract fun analyzeBlocking(data: Input, state: State): Output
}
/**
* An implementation of an analyzer factory that does not use suspending functions. This allows interoperability with
* java.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
abstract class BlockingAnalyzerFactory> : AnalyzerFactory {
override suspend fun newInstance(): AnalyzerType? = newInstanceBlocking()
abstract fun newInstanceBlocking(): AnalyzerType?
}
/**
* An implementation of an analyzer pool factory that does not use suspending functions. This allows interoperability
* with java.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
class BlockingAnalyzerPoolFactory @JvmOverloads constructor(
private val analyzerFactory: AnalyzerFactory>,
private val desiredAnalyzerCount: Int = DEFAULT_ANALYZER_PARALLEL_COUNT
) {
fun buildAnalyzerPool() = AnalyzerPool(
desiredAnalyzerCount = desiredAnalyzerCount,
analyzers = (0 until desiredAnalyzerCount).mapNotNull {
runBlocking { analyzerFactory.newInstance() }
}
)
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/interop/BlockingResult.kt
================================================
package com.getbouncer.scan.framework.interop
import com.getbouncer.scan.framework.AggregateResultListener
import com.getbouncer.scan.framework.ResultAggregator
import com.getbouncer.scan.framework.ResultHandler
import com.getbouncer.scan.framework.StatefulResultHandler
import com.getbouncer.scan.framework.TerminatingResultHandler
/**
* An implementation of a result handler that does not use suspending functions. This allows interoperability with java.
*/
abstract class BlockingResultHandler : ResultHandler {
override suspend fun onResult(result: Output, data: Input) = onResultBlocking(result, data)
abstract fun onResultBlocking(result: Output, data: Input): Verdict
}
/**
* An implementation of a stateful result handler that does not use suspending functions. This allows interoperability
* with java.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
abstract class BlockingStatefulResultHandler(
initialState: State
) : StatefulResultHandler(initialState) {
override suspend fun onResult(result: Output, data: Input): Verdict = onResultBlocking(result, data)
abstract fun onResultBlocking(result: Output, data: Input): Verdict
}
/**
* An implementation of a terminating result handler that does not use suspending functions. This allows
* interoperability with java.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
abstract class BlockingTerminatingResultHandler(
initialState: State
) : TerminatingResultHandler(initialState) {
override suspend fun onResult(result: Output, data: Input) = onResultBlocking(result, data)
override suspend fun onTerminatedEarly() = onTerminatedEarlyBlocking()
override suspend fun onAllDataProcessed() = onAllDataProcessedBlocking()
abstract fun onResultBlocking(result: Output, data: Input)
abstract fun onTerminatedEarlyBlocking()
abstract fun onAllDataProcessedBlocking()
}
/**
* An implementation of a result listener that does not use suspending functions. This allows interoperability with
* java.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
abstract class BlockingAggregateResultListener :
AggregateResultListener {
override suspend fun onInterimResult(result: InterimResult) = onInterimResultBlocking(result)
override suspend fun onResult(result: FinalResult) = onResultBlocking(result)
override suspend fun onReset() = onResetBlocking()
abstract fun onInterimResultBlocking(result: InterimResult)
abstract fun onResultBlocking(result: FinalResult)
abstract fun onResetBlocking()
}
/**
* An implementation of a result aggregator that does not use suspending functions. This allows interoperability with
* java.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
abstract class BlockingResultAggregator(
listener: AggregateResultListener,
initialState: State
) : ResultAggregator(listener, initialState) {
override suspend fun aggregateResult(frame: DataFrame, result: AnalyzerResult): Pair =
aggregateResultBlocking(frame, result)
abstract fun aggregateResultBlocking(frame: DataFrame, result: AnalyzerResult): Pair
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/interop/JavaContinuation.kt
================================================
@file:JvmName("Coroutine")
package com.getbouncer.scan.framework.interop
import android.util.Log
import com.getbouncer.scan.framework.Config
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
/**
* A utility class for calling suspend functions from java. This allows listening to a suspend function with callbacks.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
abstract class JavaContinuation @JvmOverloads constructor(
runOn: CoroutineContext = Dispatchers.Default,
private val listenOn: CoroutineContext = Dispatchers.Main
) : Continuation {
override val context: CoroutineContext = runOn
abstract fun onComplete(value: T)
abstract fun onException(exception: Throwable)
override fun resumeWith(result: Result) = result.fold(
onSuccess = {
runBlocking(listenOn) {
onComplete(it)
}
},
onFailure = {
runBlocking(listenOn) {
onException(it)
}
}
)
}
/**
* An empty continuation for ignoring results.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
class EmptyJavaContinuation : JavaContinuation() {
override fun onComplete(value: T) { }
override fun onException(exception: Throwable) {
Log.e(Config.logTag, "Error in continuation", exception)
}
}
/**
* Resume a continuation with a value.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Continuation.resumeJava(value: T) = this.resume(value)
/**
* Resume a continuation with an exception.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Continuation.resumeWithExceptionJava(exception: Throwable) = this.resumeWithException(exception)
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/ml/ModelVersionTracker.kt
================================================
package com.getbouncer.scan.framework.ml
private val MODEL_MAP = mutableMapOf>>()
/**
* Details about a model loaded into memory.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class ModelLoadDetails(
val modelClass: String,
val modelVersion: String,
val modelFrameworkVersion: Int,
val success: Boolean
)
/**
* When a ML model is loaded into memory, track the details of the model.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun trackModelLoaded(modelClass: String, modelVersion: String, modelFrameworkVersion: Int, success: Boolean) {
MODEL_MAP.getOrPut(modelClass) { mutableSetOf() }.add(Triple(modelVersion, modelFrameworkVersion, success))
}
/**
* Get the full list of models that were loaded into memory during this session.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun getLoadedModelVersions(): List = MODEL_MAP.flatMap { entry ->
entry.value.map {
ModelLoadDetails(
modelClass = entry.key,
modelVersion = it.first,
modelFrameworkVersion = it.second,
success = it.third
)
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/ml/NonMaximumSuppression.kt
================================================
package com.getbouncer.scan.framework.ml
import com.getbouncer.scan.framework.ml.ssd.RectForm
import com.getbouncer.scan.framework.ml.ssd.areaClamped
import com.getbouncer.scan.framework.ml.ssd.overlapWith
import java.util.ArrayList
/**
* In this project we implement HARD NMS and NOT Soft NMS. I highly recommend checkout SOFT NMS
* implementation of Facebook Detectron Framework.
*
* See https://towardsdatascience.com/non-maximum-suppression-nms-93ce178e177c
*
* @param boxes: Detected boxes
* @param probabilities: Probabilities of the given boxes
* @param iouThreshold: intersection over union threshold.
* @param limit: keep this number of results. If limit <= 0, keep all the results.
*
* @return pickedIndices: a list of indexes of the kept boxes
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun hardNonMaximumSuppression(
boxes: Array,
probabilities: FloatArray,
iouThreshold: Float,
limit: Int?
): ArrayList {
val indexArray = probabilities.indices.sortedByDescending { probabilities[it] }.take(200).toMutableList()
val pickedIndexes = ArrayList()
while (indexArray.isNotEmpty()) {
val current = indexArray.removeAt(0)
pickedIndexes.add(current)
if (pickedIndexes.size == limit) {
return pickedIndexes
}
val iterator = indexArray.iterator()
while (iterator.hasNext()) {
if (intersectionOverUnionOf(boxes[current], boxes[iterator.next()]) >= iouThreshold) {
iterator.remove()
}
}
}
return pickedIndexes
}
/**
* Return intersection-over-union (Jaccard index) of boxes.
*
* Args:
* boxes0 (N, 4): ground truth boxes.
* boxes1 (N or 1, 4): predicted boxes.
* eps: a small number to avoid 0 as denominator.
* Returns: iou (N): IOU values
*/
private fun intersectionOverUnionOf(currentBox: RectForm, nextBox: RectForm): Float {
val eps = 0.00001f
val overlapArea = nextBox.overlapWith(currentBox).areaClamped()
val nextArea = nextBox.areaClamped()
val currentArea = currentBox.areaClamped()
return overlapArea / (nextArea + currentArea - overlapArea + eps)
}
/**
* Runs greedy NonMaxSuppression over the raw predictions. Greedy NMS looks for the local maximas
* ("peaks") in the prediction confidences of the consecutive same predictions, keeps those,
* and replaces the other values as the background class.
*
* Example: given the following [rawPredictions] and [confidence] pair
* [rawPredictions]: [LABEL0, LABEL0, LABEL0, LABEL1, LABEL1, LABEL1]
* [confidence]: [0.1, 0.2, 0.4, 0.3, 0.5, 0.3]
* Output: [BACKGROUND, BACKGROUND, LABEL0, BACKGROUND, LABEL, BACKGROUND]
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun greedyNonMaxSuppression(
rawPredictions: Array,
confidence: FloatArray,
backgroundClass: Input
): Array {
val digits = rawPredictions.clone()
// greedy non max suppression
for (idx in 0 until digits.size - 1) {
if (digits[idx] != backgroundClass && digits[idx + 1] != backgroundClass) {
if (confidence[idx] < confidence[idx + 1]) {
digits[idx] = backgroundClass
} else {
digits[idx + 1] = backgroundClass
}
}
}
return digits
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/ml/TensorFlowLiteAnalyzer.kt
================================================
package com.getbouncer.scan.framework.ml
import android.content.Context
import android.util.Log
import com.getbouncer.scan.framework.Analyzer
import com.getbouncer.scan.framework.AnalyzerFactory
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.framework.FetchedData
import com.getbouncer.scan.framework.Loader
import com.getbouncer.scan.framework.time.Timer
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.tensorflow.lite.Interpreter
import org.tensorflow.lite.nnapi.NnApiDelegate
import java.io.Closeable
import java.nio.ByteBuffer
/**
* A TensorFlowLite analyzer uses an [Interpreter] to analyze data.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
abstract class TensorFlowLiteAnalyzer(
private val tfInterpreter: Interpreter,
private val delegate: NnApiDelegate? = null,
) : Analyzer, Closeable {
protected abstract suspend fun interpretMLOutput(data: Input, mlOutput: MLOutput): Output
protected abstract suspend fun transformData(data: Input): MLInput
protected abstract suspend fun executeInference(tfInterpreter: Interpreter, data: MLInput): MLOutput
private val loggingTimer by lazy {
Timer.newInstance(Config.logTag, this::class.java.simpleName)
}
override suspend fun analyze(data: Input, state: Any): Output {
val mlInput = loggingTimer.measureSuspend("transform") {
transformData(data)
}
val mlOutput = loggingTimer.measureSuspend("infer") {
executeInference(tfInterpreter, mlInput)
}
return loggingTimer.measureSuspend("interpret") {
interpretMLOutput(data, mlOutput)
}
}
override fun close() {
tfInterpreter.close()
delegate?.close()
}
}
/**
* A factory that creates tensorflow models as analyzers.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
abstract class TFLAnalyzerFactory>(
private val context: Context,
private val fetchedModel: FetchedData,
) : AnalyzerFactory {
protected abstract val tfOptions: Interpreter.Options
private val loader by lazy { Loader(context) }
private val loadModelMutex = Mutex()
private var loadedModel: ByteBuffer? = null
protected suspend fun createInterpreter(): Interpreter? =
createInterpreter(fetchedModel)
private suspend fun createInterpreter(fetchedModel: FetchedData): Interpreter? = try {
loadModel(fetchedModel)?.let { Interpreter(it, tfOptions) }
} catch (t: Throwable) {
Log.e(Config.logTag, "Error occurred while loading model ${fetchedModel.modelClass} version ${fetchedModel.modelVersion}", t)
null
}.apply {
if (this == null) {
Log.w(Config.logTag, "Unable to load model ${fetchedModel.modelClass} version ${fetchedModel.modelVersion}")
}
}
private suspend fun loadModel(fetchedModel: FetchedData): ByteBuffer? = loadModelMutex.withLock {
loadedModel ?: run { loader.loadData(fetchedModel) }
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/ml/ssd/ClassifierScores.kt
================================================
package com.getbouncer.scan.framework.ml.ssd
import com.getbouncer.scan.framework.util.updateEach
import kotlin.math.exp
typealias ClassifierScores = FloatArray
/**
* Compute softmax for the given row. This will replace each row value with a value normalized by
* the sum of all the values in the row.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun ClassifierScores.softMax() {
val rowSumExp = this.fold(0F) { acc, element -> acc + exp(element) }
this.updateEach { exp(it) / rowSumExp }
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/ml/ssd/RectForm.kt
================================================
package com.getbouncer.scan.framework.ml.ssd
import android.graphics.RectF
import com.getbouncer.scan.framework.util.clamp
/**
* An array of four floats, which denote a rectangle of the following values:
* [0] = left percent
* [1] = top percent
* [2] = right percent
* [3] = bottom percent
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
typealias RectForm = FloatArray
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
const val RECT_FORM_SIZE = 4
/**
* Create a new [RectForm].
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun rectForm(left: Float, top: Float, right: Float, bottom: Float) =
RectForm(RECT_FORM_SIZE).apply {
setLeft(left)
setTop(top)
setRight(right)
setBottom(bottom)
}
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectForm.left() = this[0]
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectForm.top() = this[1]
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectForm.right() = this[2]
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectForm.bottom() = this[3]
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectForm.setLeft(left: Float) { this[0] = left }
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectForm.setTop(top: Float) { this[1] = top }
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectForm.setRight(right: Float) { this[2] = right }
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectForm.setBottom(bottom: Float) { this[3] = bottom }
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectForm.calcWidth() = right() - left()
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectForm.calcHeight() = bottom() - top()
/**
* Convert this [RectForm] to a [RectF].
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectForm.toRectF() = RectF(left(), top(), right(), bottom())
/**
* Calculate the area of a rectangle while clamping the width and height between 0 and 1000.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectForm.areaClamped() = clamp(calcWidth(), 0F, 1000F) * clamp(calcHeight(), 0F, 1000F)
/**
* Create a rectangle of the overlap of this rectangle and another. Note that if the two rectangles
* do not overlap, this can create a negative area rectangle.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectForm.overlapWith(other: RectForm) =
rectForm(
kotlin.math.max(this.left(), other.left()),
kotlin.math.max(this.top(), other.top()),
kotlin.math.min(this.right(), other.right()),
kotlin.math.min(this.bottom(), other.bottom())
)
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/ml/ssd/SizeAndCenter.kt
================================================
package com.getbouncer.scan.framework.ml.ssd
import com.getbouncer.scan.framework.util.clamp
import kotlin.math.exp
/**
* An array of four floats, which denote a rectangle of the following values:
* [0] = centerX percent
* [1] = centerY percent
* [2] = width percent
* [3] = height percent
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
typealias SizeAndCenter = FloatArray
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
const val SIZE_AND_CENTER_SIZE = 4
/**
* Create a new SizeAndCenter.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun sizeAndCenter(centerX: Float, centerY: Float, width: Float, height: Float) =
SizeAndCenter(SIZE_AND_CENTER_SIZE).apply {
setCenterX(centerX)
setCenterY(centerY)
setWidth(width)
setHeight(height)
}
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeAndCenter.centerX() = this[0]
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeAndCenter.centerY() = this[1]
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeAndCenter.width() = this[2]
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeAndCenter.height() = this[3]
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeAndCenter.setCenterX(centerX: Float) { this[0] = centerX }
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeAndCenter.setCenterY(centerY: Float) { this[1] = centerY }
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeAndCenter.setWidth(width: Float) { this[2] = width }
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeAndCenter.setHeight(height: Float) { this[3] = height }
/**
* Convert [SizeAndCenter] (centerX, centerY, w, h) to [RectForm] (left, top, right, bottom)
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeAndCenter.toRectForm() {
val left = centerX() - width() / 2
val top = centerY() - height() / 2
val right = centerX() + width() / 2
val bottom = centerY() + height() / 2
setLeft(left)
setTop(top)
setRight(right)
setBottom(bottom)
}
/**
* Clamp all values in the array to the specified [minimum] and [maximum].
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeAndCenter.clampAll(minimum: Float, maximum: Float) {
setCenterX(clamp(centerX(), minimum, maximum))
setCenterY(clamp(centerY(), minimum, maximum))
setWidth(clamp(width(), minimum, maximum))
setHeight(clamp(height(), minimum, maximum))
}
/**
* Convert a regressional location result of SSD into a []SizeAndCenter].
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeAndCenter.adjustLocation(
prior: SizeAndCenter,
centerVariance: Float,
sizeVariance: Float
) {
setCenterX(centerX() * centerVariance * prior.width() + prior.centerX())
setCenterY(centerY() * centerVariance * prior.height() + prior.centerY())
setWidth(exp(width() * sizeVariance) * prior.width())
setHeight(exp(height() * sizeVariance) * prior.height())
}
/**
* Convert regressional location results of SSD into [SizeAndCenter] arrays.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Array.adjustLocations(
priors: Array,
centerVariance: Float,
sizeVariance: Float
) {
for (i in this.indices) {
this[i].adjustLocation(priors[i], centerVariance, sizeVariance)
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/time/Clock.kt
================================================
package com.getbouncer.scan.framework.time
import androidx.annotation.CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
object Clock {
@JvmStatic
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun markNow(): ClockMark = PreciseClockMark(System.nanoTime())
}
/**
* Convert a milliseconds since epoch timestamp to a clock mark.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Long.asEpochMillisecondsClockMark(): ClockMark = AbsoluteClockMark(this)
/**
* A marked point in time.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
sealed class ClockMark {
abstract fun elapsedSince(): Duration
abstract fun toMillisecondsSinceEpoch(): Long
abstract fun hasPassed(): Boolean
abstract fun isInFuture(): Boolean
abstract operator fun plus(duration: Duration): ClockMark
abstract operator fun minus(duration: Duration): ClockMark
abstract operator fun compareTo(other: ClockMark): Int
}
/**
* A clock mark based on milliseconds since epoch. This is precise to the nearest millisecond.
*/
private class AbsoluteClockMark(private val millisecondsSinceEpoch: Long) : ClockMark() {
override fun elapsedSince(): Duration = (System.currentTimeMillis() - millisecondsSinceEpoch).milliseconds
override fun toMillisecondsSinceEpoch(): Long = millisecondsSinceEpoch
override fun hasPassed(): Boolean = elapsedSince() > Duration.ZERO
override fun isInFuture(): Boolean = elapsedSince() < Duration.ZERO
override fun plus(duration: Duration): ClockMark =
AbsoluteClockMark(millisecondsSinceEpoch + duration.inMilliseconds.toLong())
override fun minus(duration: Duration): ClockMark =
AbsoluteClockMark(millisecondsSinceEpoch - duration.inMilliseconds.toLong())
override fun compareTo(other: ClockMark): Int =
millisecondsSinceEpoch.compareTo(other.toMillisecondsSinceEpoch())
override fun equals(other: Any?): Boolean =
this === other || when (other) {
is AbsoluteClockMark -> millisecondsSinceEpoch == other.millisecondsSinceEpoch
is ClockMark -> toMillisecondsSinceEpoch() == other.toMillisecondsSinceEpoch()
else -> false
}
override fun hashCode(): Int {
return millisecondsSinceEpoch.hashCode()
}
override fun toString(): String {
return "AbsoluteClockMark(at $millisecondsSinceEpoch ms since epoch})"
}
}
/**
* A precise clock mark that is not bound to epoch seconds. This is precise to the nearest nanosecond.
*/
private class PreciseClockMark(private val originMarkNanoseconds: Long) : ClockMark() {
override fun elapsedSince(): Duration = (System.nanoTime() - originMarkNanoseconds).nanoseconds
override fun toMillisecondsSinceEpoch(): Long = System.currentTimeMillis() - elapsedSince().inMilliseconds.toLong()
override fun hasPassed(): Boolean = elapsedSince() > Duration.ZERO
override fun isInFuture(): Boolean = elapsedSince() < Duration.ZERO
override fun plus(duration: Duration): ClockMark =
PreciseClockMark(originMarkNanoseconds + duration.inNanoseconds)
override fun minus(duration: Duration): ClockMark =
PreciseClockMark(originMarkNanoseconds + duration.inNanoseconds)
override fun compareTo(other: ClockMark): Int = elapsedSince().compareTo(other.elapsedSince())
override fun equals(other: Any?): Boolean =
this === other || when (other) {
is PreciseClockMark -> originMarkNanoseconds == other.originMarkNanoseconds
is ClockMark -> toMillisecondsSinceEpoch() == other.toMillisecondsSinceEpoch()
else -> false
}
override fun hashCode(): Int {
return originMarkNanoseconds.hashCode()
}
override fun toString(): String = elapsedSince().let {
if (it >= Duration.ZERO) {
"PreciseClockMark($it ago)"
} else {
"PreciseClockMark(${-it} in the future)"
}
}
}
/**
* Measure the amount of time a process takes.
*
* TODO: use contracts when they are no longer experimental
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
inline fun measureTime(block: () -> T): Pair {
// contract { callsInPlace(block, EXACTLY_ONCE) }
val mark = Clock.markNow()
val result = block()
return mark.elapsedSince() to result
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/time/Coroutine.kt
================================================
package com.getbouncer.scan.framework.time
import kotlin.math.roundToLong
/**
* Allow delaying for a specified duration
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
suspend fun delay(duration: Duration) =
kotlinx.coroutines.delay(duration.inMilliseconds.roundToLong())
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/time/Duration.kt
================================================
package com.getbouncer.scan.framework.time
import kotlin.math.round
import kotlin.math.roundToLong
/**
* Round a number to a specified number of digits.
*/
private fun Double.roundTo(numberOfDigits: Int): Double {
var multiplier = 1.0F
repeat(numberOfDigits) { multiplier *= 10 }
return round(this * multiplier) / multiplier
}
/**
* Since kotlin time is still experimental, implement our own version for utility.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
sealed class Duration : Comparable {
companion object {
val ZERO: Duration = DurationNanoseconds(0)
val INFINITE: Duration = DurationInfinitePositive
val NEGATIVE_INFINITE: Duration = DurationInfiniteNegative
}
abstract val inYears: Double
abstract val inMonths: Double
abstract val inWeeks: Double
abstract val inDays: Double
abstract val inHours: Double
abstract val inMinutes: Double
abstract val inSeconds: Double
abstract val inMilliseconds: Double
abstract val inMicroseconds: Double
abstract val inNanoseconds: Long
override fun equals(other: Any?): Boolean =
if (other is Duration) inNanoseconds == other.inNanoseconds else false
override fun hashCode(): Int = inNanoseconds.toInt()
override fun toString(): String = when {
inYears > 1 -> "${inYears.roundTo(2)} years"
inMonths > 1 -> "${inMonths.roundTo(2)} months"
inWeeks > 1 -> "${inWeeks.roundTo(2)} weeks"
inDays > 1 -> "${inDays.roundTo(2)} days"
inHours > 1 -> "${inHours.roundTo(2)} hours"
inMinutes > 1 -> "${inMinutes.roundTo(2)} minutes"
inSeconds > 1 -> "${inSeconds.roundTo(2)} seconds"
inMilliseconds > 1 -> "${inMilliseconds.roundTo(2)} milliseconds"
inMicroseconds > 1 -> "${inMicroseconds.roundTo(2)} microseconds"
else -> "$inNanoseconds nanoseconds"
}
open operator fun plus(other: Duration): Duration = DurationNanoseconds(inNanoseconds + other.inNanoseconds)
open operator fun minus(other: Duration): Duration = DurationNanoseconds(inNanoseconds - other.inNanoseconds)
open operator fun times(multiplier: Int): Duration = DurationNanoseconds(inNanoseconds * multiplier)
open operator fun times(multiplier: Long): Duration = DurationNanoseconds(inNanoseconds * multiplier)
open operator fun times(multiplier: Float): Duration = DurationNanoseconds((inNanoseconds * multiplier.toDouble()).roundToLong())
open operator fun times(multiplier: Double): Duration = DurationNanoseconds((inNanoseconds * multiplier).roundToLong())
open operator fun div(denominator: Int): Duration = DurationNanoseconds(inNanoseconds / denominator)
open operator fun div(denominator: Long): Duration = DurationNanoseconds(inNanoseconds / denominator)
open operator fun div(denominator: Float): Duration = DurationNanoseconds((inNanoseconds / denominator.toDouble()).roundToLong())
open operator fun div(denominator: Double): Duration = DurationNanoseconds((inNanoseconds / denominator).roundToLong())
open operator fun unaryMinus(): Duration = DurationNanoseconds(-inNanoseconds)
override operator fun compareTo(other: Duration): Int = inNanoseconds.compareTo(other.inNanoseconds)
}
private abstract class DurationInfinite : Duration() {
override operator fun plus(other: Duration): Duration = this
override operator fun minus(other: Duration): Duration = this
override operator fun times(multiplier: Int): Duration = this
override operator fun times(multiplier: Long): Duration = this
override operator fun times(multiplier: Float): Duration = this
override operator fun times(multiplier: Double): Duration = this
override operator fun div(denominator: Int): Duration = this
override operator fun div(denominator: Long): Duration = this
override operator fun div(denominator: Float): Duration = this
override operator fun div(denominator: Double): Duration = this
}
private object DurationInfinitePositive : DurationInfinite() {
override val inYears: Double = Double.POSITIVE_INFINITY
override val inMonths: Double = Double.POSITIVE_INFINITY
override val inWeeks: Double = Double.POSITIVE_INFINITY
override val inDays: Double = Double.POSITIVE_INFINITY
override val inHours: Double = Double.POSITIVE_INFINITY
override val inMinutes: Double = Double.POSITIVE_INFINITY
override val inSeconds: Double = Double.POSITIVE_INFINITY
override val inMilliseconds: Double = Double.POSITIVE_INFINITY
override val inMicroseconds: Double = Double.POSITIVE_INFINITY
override val inNanoseconds: Long = Long.MAX_VALUE
override fun toString(): String {
return "INFINITE"
}
override operator fun unaryMinus(): Duration = DurationInfiniteNegative
}
private object DurationInfiniteNegative : DurationInfinite() {
override val inYears: Double = Double.NEGATIVE_INFINITY
override val inMonths: Double = Double.NEGATIVE_INFINITY
override val inWeeks: Double = Double.NEGATIVE_INFINITY
override val inDays: Double = Double.NEGATIVE_INFINITY
override val inHours: Double = Double.NEGATIVE_INFINITY
override val inMinutes: Double = Double.NEGATIVE_INFINITY
override val inSeconds: Double = Double.NEGATIVE_INFINITY
override val inMilliseconds: Double = Double.NEGATIVE_INFINITY
override val inMicroseconds: Double = Double.NEGATIVE_INFINITY
override val inNanoseconds: Long = Long.MIN_VALUE
override fun toString(): String {
return "Duration(NEGATIVE_INFINITE)"
}
override operator fun unaryMinus(): Duration = DurationInfiniteNegative
}
private class DurationNanoseconds(nanoseconds: Long) : Duration() {
override val inYears by lazy { (inDays / 365.25) }
override val inMonths by lazy { (inYears * 12) }
override val inWeeks by lazy { inDays / 7 }
override val inDays by lazy { inHours / 24 }
override val inHours by lazy { inMinutes / 60 }
override val inMinutes by lazy { inSeconds / 60 }
override val inSeconds by lazy { inMilliseconds / 1000 }
override val inMilliseconds by lazy { inMicroseconds / 1000 }
override val inMicroseconds by lazy { inNanoseconds / 1000.0 }
override val inNanoseconds = nanoseconds
companion object {
fun fromYears(years: Double) = fromDays(years * 365.25)
fun fromMonths(months: Double) = fromYears(months / 12)
fun fromWeeks(weeks: Double) = fromDays(weeks * 7)
fun fromDays(days: Double) = fromHours(days * 24)
fun fromHours(hours: Double) = fromMinutes(hours * 60)
fun fromMinutes(minutes: Double) = fromSeconds(minutes * 60)
fun fromSeconds(seconds: Double) = fromMilliseconds(seconds * 1000)
fun fromMilliseconds(milliseconds: Double) = fromMicroseconds(milliseconds * 1000)
fun fromMicroseconds(microseconds: Double) = fromNanoseconds((microseconds * 1000).roundToLong())
fun fromNanoseconds(nanoseconds: Long) = DurationNanoseconds(nanoseconds)
}
}
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Int.years get(): Duration = this.toDouble().years
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Int.months get(): Duration = this.toDouble().months
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Int.weeks get(): Duration = this.toDouble().weeks
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Int.days get(): Duration = this.toDouble().days
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Int.hours get(): Duration = this.toDouble().hours
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Int.minutes get(): Duration = this.toDouble().minutes
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Int.seconds get(): Duration = this.toDouble().seconds
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Int.milliseconds get(): Duration = this.toDouble().milliseconds
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Int.microseconds get(): Duration = this.toDouble().microseconds
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Int.nanoseconds get(): Duration = this.toLong().nanoseconds
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Long.years get(): Duration = this.toDouble().years
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Long.months get(): Duration = this.toDouble().months
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Long.weeks get(): Duration = this.toDouble().weeks
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Long.days get(): Duration = this.toDouble().days
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Long.hours get(): Duration = this.toDouble().hours
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Long.minutes get(): Duration = this.toDouble().minutes
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Long.seconds get(): Duration = this.toDouble().seconds
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Long.milliseconds get(): Duration = this.toDouble().milliseconds
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Long.microseconds get(): Duration = this.toDouble().microseconds
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Long.nanoseconds get(): Duration = DurationNanoseconds.fromNanoseconds(this)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Float.years get(): Duration = this.toDouble().years
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Float.months get(): Duration = this.toDouble().months
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Float.weeks get(): Duration = this.toDouble().weeks
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Float.days get(): Duration = this.toDouble().days
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Float.hours get(): Duration = this.toDouble().hours
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Float.minutes get(): Duration = this.toDouble().minutes
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Float.seconds get(): Duration = this.toDouble().seconds
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Float.milliseconds get(): Duration = this.toDouble().milliseconds
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Float.microseconds get(): Duration = this.toDouble().microseconds
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Float.nanoseconds get(): Duration = this.roundToLong().nanoseconds
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Double.years get(): Duration = DurationNanoseconds.fromYears(this)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Double.months get(): Duration = DurationNanoseconds.fromMonths(this)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Double.weeks get(): Duration = DurationNanoseconds.fromWeeks(this)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Double.days get(): Duration = DurationNanoseconds.fromDays(this)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Double.hours get(): Duration = DurationNanoseconds.fromHours(this)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Double.minutes get(): Duration = DurationNanoseconds.fromMinutes(this)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Double.seconds get(): Duration = DurationNanoseconds.fromSeconds(this)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Double.milliseconds get(): Duration = DurationNanoseconds.fromMilliseconds(this)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Double.microseconds get(): Duration = DurationNanoseconds.fromMicroseconds(this)
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
val Double.nanoseconds get(): Duration = this.roundToLong().nanoseconds
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun min(duration1: Duration, duration2: Duration): Duration =
when {
duration1 is DurationInfinitePositive -> duration2
duration1 is DurationInfiniteNegative -> duration1
duration2 is DurationInfinitePositive -> duration1
duration2 is DurationInfiniteNegative -> duration2
else -> kotlin.math.min(duration1.inNanoseconds, duration2.inNanoseconds).nanoseconds
}
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun max(duration1: Duration, duration2: Duration): Duration =
when {
duration1 is DurationInfinitePositive -> duration1
duration1 is DurationInfiniteNegative -> duration2
duration2 is DurationInfinitePositive -> duration2
duration2 is DurationInfiniteNegative -> duration1
else -> kotlin.math.max(duration1.inNanoseconds, duration2.inNanoseconds).nanoseconds
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/time/Rate.kt
================================================
package com.getbouncer.scan.framework.time
/**
* A rate of execution.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class Rate(val amount: Long, val duration: Duration) : Comparable {
override fun compareTo(other: Rate): Int {
return (other.duration / other.amount).compareTo(duration / amount)
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/time/Timer.kt
================================================
package com.getbouncer.scan.framework.time
import android.util.Log
import com.getbouncer.scan.framework.Config
import kotlinx.coroutines.runBlocking
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
sealed class Timer {
companion object {
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun newInstance(
tag: String,
name: String,
updateInterval: Duration = 2.seconds,
enabled: Boolean = Config.isDebug
) = if (enabled) {
LoggingTimer(
tag,
name,
updateInterval
)
} else {
NoOpTimer
}
}
/**
* Log the duration of a single task and return the result from that task.
*
* TODO: use contracts when they are no longer experimental
*/
fun measure(taskName: String? = null, task: () -> T): T {
// contract { callsInPlace(task, EXACTLY_ONCE) }
return runBlocking { measureSuspend(taskName) { task() } }
}
abstract suspend fun measureSuspend(taskName: String? = null, task: suspend () -> T): T
}
private object NoOpTimer : Timer() {
// TODO: use contracts when they are no longer experimental
override suspend fun measureSuspend(taskName: String?, task: suspend () -> T): T {
// contract { callsInPlace(task, EXACTLY_ONCE) }
return task()
}
}
private class LoggingTimer(
private val tag: String,
private val name: String,
private val updateInterval: Duration
) : Timer() {
private var executionCount = 0
private var executionTotalDuration = Duration.ZERO
private var updateClock = Clock.markNow()
// TODO: use contracts when they are no longer experimental
override suspend fun measureSuspend(taskName: String?, task: suspend () -> T): T {
// contract { callsInPlace(task, EXACTLY_ONCE) }
val (duration, result) = measureTime { task() }
executionCount++
executionTotalDuration += duration
if (updateClock.elapsedSince() > updateInterval) {
updateClock = Clock.markNow()
Log.d(
tag,
"$name${if (!taskName.isNullOrEmpty()) ".$taskName" else ""} executing on " +
"thread ${Thread.currentThread().name} " +
"AT ${executionCount / executionTotalDuration.inSeconds} FPS, " +
"${executionTotalDuration.inMilliseconds / executionCount} MS/F"
)
}
return result
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/util/AppDetails.kt
================================================
package com.getbouncer.scan.framework.util
import android.content.Context
import com.getbouncer.scan.framework.BuildConfig
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
data class AppDetails(
val appPackageName: String?,
val applicationId: String,
val libraryPackageName: String,
val sdkVersion: String,
val sdkVersionCode: Int,
val sdkFlavor: String,
val isDebugBuild: Boolean
) {
companion object {
@JvmStatic
fun fromContext(context: Context) = AppDetails(
appPackageName = getAppPackageName(context),
applicationId = getApplicationId(),
libraryPackageName = getLibraryPackageName(),
sdkVersion = getSdkVersion(),
sdkVersionCode = getSdkVersionCode(),
sdkFlavor = getSdkFlavor(),
isDebugBuild = isDebugBuild()
)
}
}
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun getAppPackageName(context: Context): String? = context.applicationContext.packageName
private fun getApplicationId(): String = "" // no longer available in later versions of gradle.
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun getLibraryPackageName(): String = BuildConfig.LIBRARY_PACKAGE_NAME
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun getSdkVersion(): String = BuildConfig.SDK_VERSION_STRING
private fun getSdkVersionCode(): Int = -1 // no longer available in later versions of gradle.
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun getSdkFlavor(): String = BuildConfig.BUILD_TYPE
private fun isDebugBuild(): Boolean = BuildConfig.DEBUG
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/util/ArrayExtensions.kt
================================================
package com.getbouncer.scan.framework.util
import androidx.annotation.CheckResult
import java.nio.ByteBuffer
import kotlin.math.max
import kotlin.math.min
/**
* Update an array in place with a modifier function.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Array.updateEach(operation: (original: T) -> T) {
for (i in this.indices) {
this[i] = operation(this[i])
}
}
/**
* Update a [FloatArray] in place with a modifier function.
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun FloatArray.updateEach(operation: (original: Float) -> Float) {
for (i in this.indices) {
this[i] = operation(this[i])
}
}
/**
* Filter an array to only those values specified in an index array.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
inline fun Array.filterByIndexes(indexesToKeep: IntArray) =
Array(indexesToKeep.size) { this[indexesToKeep[it]] }
/**
* Filter an array to only those values specified in an index array.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun FloatArray.filterByIndexes(indexesToKeep: IntArray) =
FloatArray(indexesToKeep.size) { this[indexesToKeep[it]] }
/**
* Flatten an array of arrays into a single array of sequential values.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Array.flatten() = if (this.isNotEmpty()) {
this.reshape(this.size * this[0].size)[0]
} else {
floatArrayOf()
}
/**
* Transpose an array of float arrays.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Array.transpose() = if (this.isNotEmpty()) {
val oldRows = this.size
val oldColumns = this[0].size
Array(oldColumns) { newRow -> FloatArray(oldRows) { newColumn -> this[newColumn][newRow] } }
} else {
this
}
/**
* Reshape a two-dimensional array. Assume all rows of the original array are the same length, and
* that the array is evenly divisible by the new columns.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Array.reshape(newColumns: Int): Array {
val oldRows = this.size
val oldColumns = if (this.isNotEmpty()) this[0].size else 0
val linearSize = oldRows * oldColumns
val newRows = linearSize / newColumns + if (linearSize % newColumns != 0) 1 else 0
var oldRow = 0
var oldColumn = 0
return Array(newRows) {
FloatArray(newColumns) {
val value = this[oldRow][oldColumn]
if (++oldColumn == oldColumns) {
oldColumn = 0
oldRow++
}
value
}
}
}
/**
* Clamp the value between min and max
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun clamp(value: Float, minimum: Float, maximum: Float): Float =
max(minimum, min(maximum, value))
/**
* Return a list of indexes that pass the filter.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun FloatArray.filteredIndexes(predicate: (Float) -> Boolean): IntArray {
val filteredIndexes = ArrayList()
for (index in this.indices) {
if (predicate(this[index])) {
filteredIndexes.add(index)
}
}
return filteredIndexes.toIntArray()
}
/**
* Divide a [ByteArray] into an array of byte arrays of a given size. If the original array is not
* evenly divisible by the [chunkSize], the last ByteArray may be smaller than the chunk size.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun ByteArray.chunk(chunkSize: Int): Array =
Array(this.size / chunkSize + if (this.size % chunkSize == 0) 0 else 1) {
copyOfRange(it * chunkSize, min((it + 1) * chunkSize, this.size))
}
/**
* Find the index of the maximum value in the array.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun FloatArray.indexOfMax(): Int? {
if (isEmpty()) {
return null
}
var maxIndex = 0
var maxValue = this[maxIndex]
for (index in this.indices) {
if (this[index] > maxValue) {
maxIndex = index
maxValue = this[index]
}
}
return maxIndex
}
/**
* Convert a [ByteBuffer] to a [ByteArray].
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun ByteBuffer.toByteArray() = ByteArray(remaining()).also { this.get(it) }
/**
* Convert a list of [ByteBuffer]s to a single [ByteArray].
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun List.toByteArray(): ByteArray {
val totalSize = this.sumOf { it.remaining() }
var offset = 0
return ByteArray(totalSize).apply {
// This should be using this@toByteArray.forEach, but doing so seems to require API 24. It's unclear why this
// won't use the kotlin.collections version of `forEach`, but it's not during compile.
for (it in this@toByteArray) {
val size = it.remaining()
it.get(this, offset, size)
offset += size
}
}
}
/**
* Map an array to a new [Array].
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
inline fun Array.mapArray(transform: (T) -> U) = Array(this.size) { transform(this[it]) }
/**
* Map an array to a new [IntArray].
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Array.mapToIntArray(transform: (T) -> Int) = IntArray(this.size) { transform(this[it]) }
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/util/Device.kt
================================================
package com.getbouncer.scan.framework.util
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.provider.Settings
import android.telephony.TelephonyManager
import java.util.Locale
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class Device(
val ids: DeviceIds,
val name: String,
val bootCount: Int,
val locale: String?,
val carrier: String?,
val networkOperator: String?,
val phoneType: Int?,
val phoneCount: Int,
val osVersion: Int,
val platform: String
) {
companion object {
private val getDeviceDetails = cacheFirstResult { context: Context ->
Device(
ids = DeviceIds.fromContext(context),
name = getDeviceName(),
bootCount = getDeviceBootCount(context),
locale = getDeviceLocale(),
carrier = getDeviceCarrier(context),
networkOperator = getNetworkOperator(context),
phoneType = getDevicePhoneType(context),
phoneCount = getDevicePhoneCount(context),
osVersion = getOsVersion(),
platform = getPlatform()
)
}
@JvmStatic
fun fromContext(context: Context) = getDeviceDetails(context.applicationContext)
}
}
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
data class DeviceIds(
val androidId: String?
) {
companion object {
private val getDeviceIds = cacheFirstResult { context: Context ->
DeviceIds(
androidId = getAndroidId(context)
)
}
fun fromContext(context: Context) = getDeviceIds(context.applicationContext)
}
}
@SuppressLint("HardwareIds")
private fun getAndroidId(context: Context): String? =
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
private fun getDeviceBootCount(context: Context): Int =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT)
} catch (t: Throwable) {
-1
}
} else {
-1
}
private fun getDeviceLocale(): String = "${Locale.getDefault().isO3Language}_${Locale.getDefault().isO3Country}"
private fun getDeviceCarrier(context: Context) = try {
(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?)?.networkOperatorName
} catch (t: Throwable) {
null
}
private fun getDevicePhoneType(context: Context) = try {
(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?)?.phoneType
} catch (t: Throwable) {
null
}
private fun getDevicePhoneCount(context: Context) =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?)?.activeModemCount ?: -1
} else {
@Suppress("deprecation")
(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?)?.phoneCount ?: -1
}
} catch (t: Throwable) {
-1
}
} else {
-1
}
private fun getNetworkOperator(context: Context) =
(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?)?.networkOperator
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun getOsVersion() = Build.VERSION.SDK_INT
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun getPlatform() = "android"
/**
* from https://stackoverflow.com/a/27836910/947883
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun getDeviceName(): String {
// TODO: change this back once we can support newer kotlin versions
// val manufacturer = Build.MANUFACTURER?.lowercase() ?: ""
// val model = Build.MODEL?.lowercase() ?: ""
val manufacturer = Build.MANUFACTURER?.toLowerCase(Locale.ROOT) ?: ""
val model = Build.MODEL?.toLowerCase(Locale.ROOT) ?: ""
return if (model.startsWith(manufacturer)) {
model
} else {
"$manufacturer $model"
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/util/File.kt
================================================
package com.getbouncer.scan.framework.util
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.lang.Exception
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
private val illegalFileNameCharacters = setOf('"', '*', '/', ':', '<', '>', '?', '\\', '|', '+', ',', ';', '=', '[', ']')
/**
* Sanitize the name of a file for storage
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun sanitizeFileName(unsanitized: String) =
unsanitized.map { char -> if (char in illegalFileNameCharacters) "_" else char }.joinToString("")
/**
* Determine if a [File] matches the expected [hash].
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
suspend fun fileMatchesHash(localFile: File, hash: String, hashAlgorithm: String) = try {
hash == calculateHash(localFile, hashAlgorithm)
} catch (t: Throwable) {
false
}
/**
* Calculate the hash of a file using the [hashAlgorithm].
*/
@Throws(IOException::class, NoSuchAlgorithmException::class)
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
suspend fun calculateHash(file: File, hashAlgorithm: String): String? =
withContext(Dispatchers.IO) {
if (file.exists()) {
val digest = MessageDigest.getInstance(hashAlgorithm)
FileInputStream(file).use { digest.update(it.readBytes()) }
digest.digest().joinToString("") { "%02x".format(it) }
} else {
null
}
}
/**
* A file does not match the expected hash value.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class HashMismatchException(val algorithm: String, val expected: String, val actual: String?) :
Exception("Invalid hash result for algorithm '$algorithm'. Expected '$expected' but got '$actual'") {
override fun toString() = "HashMismatchException(algorithm='$algorithm', expected='$expected', actual='$actual')"
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/util/FrameRateTracker.kt
================================================
package com.getbouncer.scan.framework.util
import android.util.Log
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.framework.time.Clock
import com.getbouncer.scan.framework.time.ClockMark
import com.getbouncer.scan.framework.time.Duration
import com.getbouncer.scan.framework.time.Rate
import com.getbouncer.scan.framework.time.seconds
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.atomic.AtomicLong
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
interface FrameRateListener {
fun onFrameRateUpdate(overallRate: Rate, instantRate: Rate)
}
/**
* A class that tracks the rate at which frames are processed. This is useful for debugging to
* determine how quickly a device is handling data.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class FrameRateTracker(
private val name: String,
private val listener: FrameRateListener? = null,
private val notifyInterval: Duration = 1.seconds,
) {
private var firstFrameTime: ClockMark? = null
private var lastNotifyTime: ClockMark = Clock.markNow()
// This is -1 so that we do not calculate a rate for the first frame
private val totalFramesProcessed: AtomicLong = AtomicLong(-1)
private val framesProcessedSinceLastUpdate: AtomicLong = AtomicLong(0)
private val frameRateMutex = Mutex()
/**
* Calculate the current rate at which frames are being processed. If the notify interval has
* elapsed, notify the listener of the current rate.
*/
suspend fun trackFrameProcessed() {
val totalFrames = totalFramesProcessed.incrementAndGet()
val framesSinceLastUpdate = framesProcessedSinceLastUpdate.incrementAndGet()
val lastNotifyTime = this.lastNotifyTime
val shouldNotifyOfFrameRate = totalFrames > 0 && frameRateMutex.withLock {
val shouldNotify = lastNotifyTime.elapsedSince() > notifyInterval
if (shouldNotify) {
this.lastNotifyTime = Clock.markNow()
}
shouldNotify
}
val firstFrameTime = this.firstFrameTime ?: Clock.markNow()
this.firstFrameTime = firstFrameTime
if (shouldNotifyOfFrameRate) {
val overallFrameRate = Rate(totalFrames, firstFrameTime.elapsedSince())
val instantFrameRate = Rate(framesSinceLastUpdate, lastNotifyTime.elapsedSince())
logProcessingRate(overallFrameRate, instantFrameRate)
listener?.onFrameRateUpdate(overallFrameRate, instantFrameRate)
framesProcessedSinceLastUpdate.set(0)
}
}
/**
* Reset the state of the frame rate tracker.
*/
fun reset() {
firstFrameTime = null
lastNotifyTime = Clock.markNow()
totalFramesProcessed.set(0)
framesProcessedSinceLastUpdate.set(0)
}
/**
* Get the average frame rate for this device
*/
fun getAverageFrameRate() = Rate(
amount = totalFramesProcessed.get(),
duration = firstFrameTime?.elapsedSince() ?: Duration.ZERO
)
/**
* The processing rate has been updated. This is useful for debugging and measuring performance.
*
* @param overallRate: The total frame rate at which the analyzer is running
* @param instantRate: The instantaneous frame rate at which the analyzer is running
*/
private fun logProcessingRate(overallRate: Rate, instantRate: Rate) {
val overallFps = if (overallRate.duration != Duration.ZERO) {
overallRate.amount / overallRate.duration.inSeconds
} else {
0.0
}
val instantFps = if (instantRate.duration != Duration.ZERO) {
instantRate.amount / instantRate.duration.inSeconds
} else {
0.0
}
if (Config.isDebug) {
Log.d(Config.logTag, "$name processing avg=$overallFps, inst=$instantFps")
}
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/util/FrameSaver.kt
================================================
package com.getbouncer.scan.framework.util
import androidx.annotation.CheckResult
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.LinkedList
/**
* Save data frames for later retrieval.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
abstract class FrameSaver {
private val saveFrameMutex = Mutex()
private val savedFrames = mutableMapOf>()
/**
* Determine how frames should be classified using [getSaveFrameIdentifier], and then store them
* in a map of frames based on that identifier.
*
* This method keeps track of the total number of saved frames. If the total number or total
* size exceeds the maximum allowed, the oldest frames will be dropped.
*/
suspend fun saveFrame(frame: Frame, metaData: MetaData) {
val identifier = getSaveFrameIdentifier(frame, metaData) ?: return
return saveFrameMutex.withLock {
val maxSavedFrames = getMaxSavedFrames(identifier)
val frames = savedFrames.getOrPut(identifier) { LinkedList() }
frames.addFirst(frame)
while (frames.size > maxSavedFrames) {
// saved frames is over size limit, reduce until it's not
removeFrame(identifier, frames)
}
}
}
/**
* Retrieve a copy of the list of saved frames.
*/
@CheckResult
fun getSavedFrames(): Map> = savedFrames.toMap()
/**
* Clear all saved frames
*/
suspend fun reset() = saveFrameMutex.withLock {
savedFrames.clear()
}
protected abstract fun getMaxSavedFrames(savedFrameIdentifier: Identifier): Int
/**
* Determine if a data frame should be saved for future processing.
*
* If this method returns a non-null string, the frame will be saved under that identifier.
*/
protected abstract fun getSaveFrameIdentifier(frame: Frame, metaData: MetaData): Identifier?
/**
* Remove a frame from this list. The most recently added frames will be at the beginning of
* this list, while the least recently added frames will be at the end.
*/
protected open fun removeFrame(identifier: Identifier, frames: LinkedList) {
frames.removeLast()
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/util/ItemCounter.kt
================================================
package com.getbouncer.scan.framework.util
import androidx.annotation.CheckResult
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.LinkedList
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
interface ItemCounter {
suspend fun countItem(item: T): Int
fun getHighestCountItem(minCount: Int = 1): Pair?
suspend fun reset()
}
/**
* A class that counts and saves items.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class ItemTotalCounter(firstValue: T? = null) : ItemCounter {
private val storageMutex = Mutex()
private val items = mutableMapOf()
init { if (firstValue != null) runBlocking { countItem(firstValue) } }
/**
* Increment the count for the given item. Return the new count for the given item.
*/
override suspend fun countItem(item: T): Int = storageMutex.withLock {
1 + (items.put(item, 1 + (items[item] ?: 0)) ?: 0)
}
/**
* Get the item that with the highest count.
*
* @param minCount the minimum times an item must have been counted.
*/
@CheckResult
override fun getHighestCountItem(minCount: Int): Pair? =
items
.maxByOrNull { it.value }
?.let { if (items[it.key] ?: 0 >= minCount) it.value to it.key else null }
/**
* Reset all item counts.
*/
override suspend fun reset() = storageMutex.withLock {
items.clear()
}
}
/**
* A class that keeps track of [maxItemsToTrack] recent items.
*/
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
class ItemRecencyCounter(
private val maxItemsToTrack: Int,
firstValue: T? = null
) : ItemCounter {
private val storageMutex = Mutex()
private val items = LinkedList()
init { if (firstValue != null) runBlocking { countItem(firstValue) } }
/**
* Increment the count for the given item. Return the new count for the given item.
*/
override suspend fun countItem(item: T): Int = storageMutex.withLock {
items.addFirst(item)
while (items.size > maxItemsToTrack) {
items.removeLast()
}
items.count { it == item }
}
/**
* Get the item that with the highest count.
*
* @param minCount the minimum times an item must have been counted.
*/
@CheckResult
override fun getHighestCountItem(minCount: Int): Pair? =
items
.groupingBy { it }
.eachCount()
.filter { it.value >= minCount }
.maxByOrNull { it.value }
?.let { it.value to it.key }
/**
* Reset all item counts.
*/
override suspend fun reset() = storageMutex.withLock {
items.clear()
}
}
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/util/Layout.kt
================================================
package com.getbouncer.scan.framework.util
import android.graphics.Rect
import android.graphics.RectF
import android.util.Size
import android.util.SizeF
import android.view.View
import androidx.annotation.CheckResult
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
/**
* Determine the maximum size of rectangle with a given aspect ratio (X/Y) that can fit inside the
* specified area.
*
* For example, if the aspect ratio is 1/2 and the area is 2x2, the resulting rectangle would be
* size 1x2 and look like this:
* ```
* ________
* | | | |
* | | | |
* | | | |
* |_|____|_|
* ```
*/
@CheckResult
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun maxAspectRatioInSize(area: Size, aspectRatio: Float): Size {
var width = area.width
var height = (width / aspectRatio).roundToInt()
return if (height <= area.height) {
Size(area.width, height)
} else {
height = area.height
width = (height * aspectRatio).roundToInt()
Size(min(width, area.width), height)
}
}
/**
* Determine the minimum size of rectangle with a given aspect ratio (X/Y) that a specified area
* can fit inside.
*
* For example, if the aspect ratio is 1/2 and the area is 1x1, the resulting rectangle would be
* size 1x2 and look like this:
* ```
* ____
* |____|
* | |
* |____|
* |____|
* ```
*/
@CheckResult
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun minAspectRatioSurroundingSize(area: Size, aspectRatio: Float): Size {
var width = area.width
var height = (width / aspectRatio).roundToInt()
return if (height >= area.height) {
Size(area.width, height)
} else {
height = area.height
width = (height * aspectRatio).roundToInt()
Size(max(width, area.width), height)
}
}
/**
* Given a size and an aspect ratio, resize the area to fit that aspect ratio. If the desired aspect
* ratio is smaller than the one of the provided size, the size will be cropped to match. If the
* desired aspect ratio is larger than the that of the provided size, then the size will be expanded
* to match.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun adjustSizeToAspectRatio(area: Size, aspectRatio: Float): Size = if (aspectRatio < 1) {
Size(area.width, (area.width / aspectRatio).roundToInt())
} else {
Size((area.height * aspectRatio).roundToInt(), area.height)
}
/**
* Calculate the position of the [Size] within the [containingSize]. This makes a few assumptions:
* 1. the [Size] and the [containingSize] are centered relative to each other.
* 2. the [Size] and the [containingSize] have the same orientation
* 3. the [containingSize] and the [Size] share either a horizontal or vertical field of view
* 4. the non-shared field of view must be smaller on the [Size] than the [containingSize]
*
* If using this to project a preview image onto a full camera image, This makes a few assumptions:
* 1. the preview image [Size] and full image [containingSize] are centered relative to each other
* 2. the preview image and the full image have the same orientation
* 3. the preview image and the full image share either a horizontal or vertical field of view
* 4. the non-shared field of view must be smaller on the preview image than the full image
*
* Note that the [Size] and the [containingSize] are allowed to have completely independent
* resolutions.
*/
@CheckResult
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun Size.scaleAndCenterWithin(containingSize: Size): Rect {
val aspectRatio = width.toFloat() / height
// Since the preview image may be at a different resolution than the full image, scale the
// preview image to be circumscribed by the fullImage.
val scaledSize = maxAspectRatioInSize(containingSize, aspectRatio)
val left = (containingSize.width - scaledSize.width) / 2
val top = (containingSize.height - scaledSize.height) / 2
return Rect(
left,
top,
left + scaledSize.width,
top + scaledSize.height,
)
}
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Size.scaleAndCenterWithin(containingRect: Rect): Rect =
this.scaleAndCenterWithin(containingRect.size()).move(containingRect.left, containingRect.top)
/**
* Calculate the position of the [Size] surrounding the [surroundedSize]. This makes a few
* assumptions:
* 1. the [Size] and the [surroundedSize] are centered relative to each other.
* 2. the [Size] and the [surroundedSize] have the same orientation
* 3. the [surroundedSize] and the [Size] share either a horizontal or vertical field of view
* 4. the non-shared field of view must be smaller on the [surroundedSize] than the [Size]
*
* If using this to project a full camera image onto a preview image, This makes a few assumptions:
* 1. the preview image [surroundedSize] and full image [Size] are centered relative to each other
* 2. the preview image and the full image have the same orientation
* 3. the preview image and the full image share either a horizontal or vertical field of view
* 4. the non-shared field of view must be smaller on the preview image than the full image
*
* Note that the [Size] and the [surroundedSize] are allowed to have completely independent
* resolutions.
*/
@CheckResult
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun Size.scaleAndCenterSurrounding(surroundedSize: Size): Rect {
val aspectRatio = width.toFloat() / height
val scaledSize = minAspectRatioSurroundingSize(surroundedSize, aspectRatio)
val left = (surroundedSize.width - scaledSize.width) / 2
val top = (surroundedSize.height - scaledSize.height) / 2
return Rect(
left,
top,
left + scaledSize.width,
top + scaledSize.height,
)
}
/**
* Scale a size based on percentage scale values, and keep track of its position.
*/
@CheckResult
@Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan")
fun Size.scaleCentered(x: Float, y: Float): Rect {
val newSize = this.scale(x, y)
val left = (this.width - newSize.width) / 2
val top = (this.height - newSize.height) / 2
return Rect(
left,
top,
left + newSize.width,
top + newSize.height,
)
}
/**
* Calculate the new size based on percentage scale values.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeF.scale(x: Float, y: Float) = SizeF(this.width * x, this.height * y)
/**
* Calculate the new size based on a percentage scale.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeF.scale(scale: Float) = this.scale(scale, scale)
/**
* Calculate the new size based on percentage scale values.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Size.scale(x: Float, y: Float): Size = Size((this.width * x).roundToInt(), (this.height * y).roundToInt())
/**
* Calculate the new size based on a percentage scale.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Size.scale(scale: Float) = this.scale(scale, scale)
/**
* Center a size on a given rectangle. The size may be larger or smaller than the rect.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Size.centerOn(rect: Rect) = Rect(
/* left */
rect.centerX() - this.width / 2,
/* top */
rect.centerY() - this.height / 2,
/* right */
rect.centerX() + this.width / 2,
/* bottom */
rect.centerY() + this.height / 2
)
/**
* Scale a [Rect] to have a size equivalent to the [scaledSize]. This will also scale the position
* of the [Rect].
*
* For example, scaling a Rect(1, 2, 3, 4) by Size(5, 6) will result in a Rect(5, 12, 15, 24)
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectF.scaled(scaledSize: Size) = RectF(
this.left * scaledSize.width,
this.top * scaledSize.height,
this.right * scaledSize.width,
this.bottom * scaledSize.height
)
/**
* Scale a [Rect] to have a size equivalent to the [scaledSize]. This will maintain the center
* position of the [Rect].
*
* For example, scaling a Rect(5, 6, 7, 8) by Size(2, 0.5) will result
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectF.centerScaled(scaleX: Float, scaleY: Float) = RectF(
this.centerX() - this.width() * scaleX / 2,
this.centerY() - this.height() * scaleY / 2,
this.centerX() + this.width() * scaleX / 2,
this.centerY() + this.height() * scaleY / 2
)
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Rect.centerScaled(scaleX: Float, scaleY: Float) = Rect(
this.centerX() - (this.width() * scaleX / 2).toInt(),
this.centerY() - (this.height() * scaleY / 2).toInt(),
this.centerX() + (this.width() * scaleX / 2).toInt(),
this.centerY() + (this.height() * scaleY / 2).toInt()
)
/**
* Converts a size to rectangle with the top left corner at 0,0
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Size.toRect() = Rect(0, 0, this.width, this.height)
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Size.toRectF() = RectF(0F, 0F, this.width.toFloat(), this.height.toFloat())
/**
* Transpose a size's width and height.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Size.transpose() = Size(this.height, this.width)
/**
* Return a rect that is the intersection of two other rects
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Rect.intersectionWith(rect: Rect): Rect {
require(this.intersect(rect)) {
"Given rects do not intersect $this <> $rect"
}
return Rect(
max(this.left, rect.left),
max(this.top, rect.top),
min(this.right, rect.right),
min(this.bottom, rect.bottom)
)
}
/**
* Move relative to its current position
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Rect.move(relativeX: Int, relativeY: Int) = Rect(
this.left + relativeX,
this.top + relativeY,
this.right + relativeX,
this.bottom + relativeY,
)
/**
* Move relative to its current position
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectF.move(relativeX: Float, relativeY: Float) = RectF(
this.left + relativeX,
this.top + relativeY,
this.right + relativeX,
this.bottom + relativeY,
)
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Size.toSizeF() = SizeF(width.toFloat(), height.toFloat())
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeF.toSize() = Size(width.roundToInt(), height.roundToInt())
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Rect.toRectF() = RectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectF.toRect() = Rect(left.roundToInt(), top.roundToInt(), right.roundToInt(), bottom.roundToInt())
/**
* Takes a relation between a region of interest and a size and projects the region of interest
* to that new location
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeF.projectRegionOfInterest(toSize: SizeF, regionOfInterest: RectF): RectF {
require(this.width > 0 && this.height > 0) {
"Cannot project from container with non-positive dimensions"
}
return RectF(
regionOfInterest.left * toSize.width / this.width,
regionOfInterest.top * toSize.height / this.height,
regionOfInterest.right * toSize.width / this.width,
regionOfInterest.bottom * toSize.height / this.height,
)
}
/**
* Takes a relation between a region of interest and a size and projects the region of interest
* to that new location
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Size.projectRegionOfInterest(toSize: Size, regionOfInterest: Rect) =
this.toSizeF().projectRegionOfInterest(toSize.toSizeF(), regionOfInterest.toRectF()).toRect()
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectF.projectRegionOfInterest(toSize: SizeF, regionOfInterest: RectF) =
this.size().projectRegionOfInterest(toSize, regionOfInterest.move(-this.left, -this.top))
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Rect.projectRegionOfInterest(toSize: Size, regionOfInterest: Rect) =
this.size().projectRegionOfInterest(toSize, regionOfInterest.move(-this.left, -this.top))
/**
* Project a region of interest from one [Rect] to another. For example, given the rect and region of interest:
* _______
* | |
* | _ |
* | |_| |
* | |
* |_______|
*
* When projected to the following region:
* ___________
* | |
* | |
* |___________|
*
* The position and size of the region of interest are scaled to the new rect.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectF.projectRegionOfInterest(toRect: RectF, regionOfInterest: RectF) =
this.projectRegionOfInterest(toRect.size(), regionOfInterest).move(toRect.left, toRect.top)
/**
* Project a region of interest from one [Rect] to another. For example, given the rect and region of interest:
* _______
* | |
* | _ |
* | |_| |
* | |
* |_______|
*
* When projected to the following region:
* ___________
* | |
* | |
* |___________|
*
* The position and size of the region of interest are scaled to the new rect.
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Rect.projectRegionOfInterest(toRect: Rect, regionOfInterest: Rect) =
this.projectRegionOfInterest(toRect.size(), regionOfInterest).move(toRect.left, toRect.top)
/**
* This method allows relocating and resizing a portion of a [Size]. It returns the required
* translations required to achieve this relocation. This is useful for zooming in on sections of
* an image.
*
* For example, given a size 5x5 and an original region (2, 2, 3, 3):
*
* _______
* | |
* | _ |
* | |_| |
* | |
* |_______|
*
* If the [newRegion] is (1, 1, 4, 4) and the [newSize] is 6x6, the result will look like this:
*
* ________
* | ___ |
* | | | |
* | | | |
* | |___| |
* | |
* |________|
*
* Nine individual translations will be returned for the affected regions. The returned [Rect]s
* will look like this:
*
* ________
* |_|___|__|
* | | | |
* | | | |
* |_|___|__|
* | | | |
* |_|___|__|
*/
@CheckResult
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Size.resizeRegion(
originalRegion: Rect,
newRegion: Rect,
newSize: Size
): Map = mapOf(
Rect(
0,
0,
originalRegion.left,
originalRegion.top
) to Rect(
0,
0,
newRegion.left,
newRegion.top
),
Rect(
originalRegion.left,
0,
originalRegion.right,
originalRegion.top
) to Rect(
newRegion.left,
0,
newRegion.right,
newRegion.top
),
Rect(
originalRegion.right,
0,
this.width,
originalRegion.top
) to Rect(
newRegion.right,
0,
newSize.width,
newRegion.top
),
Rect(
0,
originalRegion.top,
originalRegion.left,
originalRegion.bottom
) to Rect(
0,
newRegion.top,
newRegion.left,
newRegion.bottom
),
Rect(
originalRegion.left,
originalRegion.top,
originalRegion.right,
originalRegion.bottom
) to Rect(
newRegion.left,
newRegion.top,
newRegion.right,
newRegion.bottom
),
Rect(
originalRegion.right,
originalRegion.top,
this.width,
originalRegion.bottom
) to Rect(
newRegion.right,
newRegion.top,
newSize.width,
newRegion.bottom
),
Rect(
0,
originalRegion.bottom,
originalRegion.left,
this.height
) to Rect(
0,
newRegion.bottom,
newRegion.left,
newSize.height
),
Rect(
originalRegion.left,
originalRegion.bottom,
originalRegion.right,
this.height
) to Rect(
newRegion.left,
newRegion.bottom,
newRegion.right,
newSize.height
),
Rect(
originalRegion.right,
originalRegion.bottom,
this.width,
this.height
) to Rect(
newRegion.right,
newRegion.bottom,
newSize.width,
newSize.height
)
)
/**
* Determine the size of a [Rect].
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Rect.size() = Size(width(), height())
/**
* Determine the size of a [RectF].
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun RectF.size() = SizeF(width(), height())
/**
* Determine the aspect ratio of a [Size].
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun Size.aspectRatio() = width.toFloat() / height.toFloat()
/**
* Determine the aspect ratio of a [SizeF].
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun SizeF.aspectRatio() = width / height
/**
* Determine the size of a [View].
*/
@Deprecated(
message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan",
replaceWith = ReplaceWith("StripeCardScan"),
)
fun View.size() = Size(width, height)
================================================
FILE: scan-framework/src/main/java/com/getbouncer/scan/framework/util/Memoize.kt
================================================
package com.getbouncer.scan.framework.util
import com.getbouncer.scan.framework.time.Clock
import com.getbouncer.scan.framework.time.ClockMark
import com.getbouncer.scan.framework.time.Duration
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* A symbol for identifying values that have not yet been initialized.
*/
private object UninitializedValue
/**
* A class that memoizes the result of a suspend function. Only one coroutine will ever perform the
* work in the suspend function, others will suspend until a result is available, and then return
* that result.
*/
private class MemoizeSuspend0(private val f: suspend () -> Result) {
private val initializeMutex = Mutex()
@Volatile private var value: Any? = UninitializedValue
fun memoize(): suspend () -> Result = {
initializeMutex.withLock {
if (value == UninitializedValue) {
value = f()
}
@Suppress("UNCHECKED_CAST") (value as Result)
}
}
}
/**
* A class that memoizes the result of a suspend function. Only one coroutine will ever perform the
* work in the suspend function, others will suspend until a result is available, and then return
* that result.
*/
private class MemoizeSuspend1(private val f: suspend (Input) -> Result) {
private val lookupMutex = Mutex()
private val values = mutableMapOf()
private val mutexes = mutableMapOf()
private suspend fun getMutex(input: Input): Mutex = lookupMutex.withLock {
mutexes.getOrPut(input) { Mutex() }
}
fun memoize(): suspend (Input) -> Result = { input ->
getMutex(input).withLock {
values.getOrPut(input) { f(input) }
}
}
}
/**
* A class that memoizes the result of a suspend function. Only one coroutine will ever perform the
* work in the suspend function, others will suspend until a result is available, and then return
* that result.
*/
private class MemoizeSuspend2(
private val f: suspend (Input1, Input2) -> Result
) {
private val lookupMutex = Mutex()
private val values = mutableMapOf, Result>()
private val mutexes = mutableMapOf, Mutex>()
private suspend fun getMutex(input1: Input1, input2: Input2): Mutex = lookupMutex.withLock {
mutexes.getOrPut(input1 to input2) { Mutex() }
}
fun memoize(): suspend (Input1, Input2) -> Result = { input1, input2 ->
getMutex(input1, input2).withLock {
values.getOrPut(input1 to input2) { f(input1, input2) }
}
}
}
/**
* A class that memoizes the result of a suspend function. Only one coroutine will ever perform the
* work in the suspend function, others will suspend until a result is available, and then return
* that result.
*/
private class MemoizeSuspend3(
private val f: suspend (Input1, Input2, Input3) -> Result
) {
private val lookupMutex = Mutex()
private val values = mutableMapOf, Result>()
private val mutexes = mutableMapOf, Mutex>()
private suspend fun getMutex(input1: Input1, input2: Input2, input3: Input3): Mutex = lookupMutex.withLock {
mutexes.getOrPut(Triple(input1, input2, input3)) { Mutex() }
}
fun memoize(): suspend (Input1, Input2, Input3) -> Result = { input1, input2, input3 ->
getMutex(input1, input2, input3).withLock {
values.getOrPut(Triple(input1, input2, input3)) { f(input1, input2, input3) }
}
}
}
/**
* A class that memoizes the result of a function. This method is threadsafe. Only one thread will
* ever invoke the backing function, other threads will block until a result is available, and then
* return that result. The result will expire after the defined timeout, at which point the function
* can be executed again.
*/
private class MemoizeSuspendExpiring0(
private val validFor: Duration,
private val f: suspend () -> Result,
) {
private val initializeMutex = Mutex()
@Volatile private var value: Any? = UninitializedValue
private var expiration: ClockMark? = null
fun memoize(): suspend () -> Result = {
initializeMutex.withLock {
if (value == UninitializedValue || expiration?.hasPassed() != false) {
value = f()
expiration = Clock.markNow() + validFor
}
@Suppress("UNCHECKED_CAST") (value as Result)
}
}
}
/**
* A class that memoizes the result of a function. This method is threadsafe. Only one thread will
* ever invoke the backing function, other threads will block until a result is available, and then
* return that result. The result will expire after the defined timeout, at which point the function
* can be executed again.
*/
private class MemoizeSuspendExpiring1(
private val validFor: Duration,
private val f: suspend (Input) -> Result,
) {
private val lookupMutex = Mutex()
private val values = mutableMapOf>()
private val mutexes = mutableMapOf()
private suspend fun getMutex(input: Input): Mutex = lookupMutex.withLock {
mutexes.getOrPut(input) { Mutex() }
}
fun memoize(): suspend (Input) -> Result = { input ->
getMutex(input).withLock {
val (result, expiration) = values[input] ?: UninitializedValue to null
if (result == UninitializedValue || expiration?.hasPassed() != false) {
val computedResult = f(input)
values[input] = computedResult to Clock.markNow() + validFor
computedResult
} else {
@Suppress("UNCHECKED_CAST") (result as Result)
}
}
}
}
/**
* A class that memoizes the result of a function. This method is threadsafe. Only one thread will
* ever invoke the backing function, other threads will block until a result is available, and then
* return that result. The result will expire after the defined timeout, at which point the function
* can be executed again.
*/
private class MemoizeSuspendExpiring2(
private val validFor: Duration,
private val f: suspend (Input1, Input2) -> Result,
) {
private val lookupMutex = Mutex()
private val values = mutableMapOf, Pair>()
private val mutexes = mutableMapOf, Mutex>()
private suspend fun getMutex(input1: Input1, input2: Input2): Mutex = lookupMutex.withLock {
mutexes.getOrPut(input1 to input2) { Mutex() }
}
fun memoize(): suspend (Input1, Input2) -> Result = { input1, input2 ->
getMutex(input1, input2).withLock {
val (result, expiration) = values[input1 to input2] ?: UninitializedValue to null
if (result == UninitializedValue || expiration?.hasPassed() != false) {
val computedResult = f(input1, input2)
values[input1 to input2] = computedResult to Clock.markNow() + validFor
computedResult
} else {
@Suppress("UNCHECKED_CAST") (result as Result)
}
}
}
}
/**
* A class that memoizes the result of a function. This method is threadsafe. Only one thread will
* ever invoke the backing function, other threads will block until a result is available, and then
* return that result. The result will expire after the defined timeout, at which point the function
* can be executed again.
*/
private class MemoizeSuspendExpiring3(
private val validFor: Duration,
private val f: suspend (Input1, Input2, Input3) -> Result,
) {
private val values = mutableMapOf, Pair>()
private val mutexes = mutableMapOf, Mutex>()
@Synchronized
private fun getMutex(input1: Input1, input2: Input2, input3: Input3): Mutex =
mutexes.getOrPut(Triple(input1, input2, input3)) { Mutex() }
fun memoize(): suspend (Input1, Input2, Input3) -> Result = { input1, input2, input3 ->
getMutex(input1, input2, input3).withLock {
val (result, expiration) = values[Triple(input1, input2, input3)] ?: UninitializedValue to null
if (result == UninitializedValue || expiration?.hasPassed() != false) {
val computedResult = f(input1, input2, input3)
values[Triple(input1, input2, input3)] = computedResult to Clock.markNow() + validFor
computedResult
} else {
@Suppress("UNCHECKED_CAST") (result as Result)
}
}
}
}
/**
* A class that memoizes the result of a function. This method is threadsafe. Only one thread will
* ever invoke the backing function, other threads will block until a result is available, and then
* return that result.
*/
private class Memoize0(private val function: () -> Result) : () -> Result {
@Volatile private var value: Any? = UninitializedValue
@Synchronized
override fun invoke(): Result {
if (value == UninitializedValue) {
value = function()
}
@Suppress("UNCHECKED_CAST") return (value as Result)
}
}
/**
* A class that memoizes the result of a function. This method is threadsafe. Only one thread will
* ever invoke the backing function, other threads will block until a result is available, and then
* return that result.
*/
private class Memoize1(
private val function: (Input) -> Result
) : (Input) -> Result {
private val values = mutableMapOf()
private val locks = mutableMapOf()
@Synchronized
private fun getLock(input: Input): Any = locks.getOrPut(input) { Object() }
override fun invoke(input: Input): Result {
val lock = getLock(input)
return synchronized(lock) {
values.getOrPut(input) { function(input) }
}
}
}
/**
* A class that memoizes the result of a function. This method is threadsafe. Only one thread will
* ever invoke the backing function, other threads will block until a result is available, and then
* return that result.
*/
private class Memoize2(
private val function: (Input1, Input2) -> Result
) : (Input1, Input2) -> Result {
private val values = mutableMapOf, Result>()
private val locks = mutableMapOf, Any>()
@Synchronized
private fun getLock(input1: Input1, input2: Input2): Any =
locks.getOrPut(input1 to input2) { Object() }
override fun invoke(input1: Input1, input2: Input2): Result {
val lock = getLock(input1, input2)
return synchronized(lock) {
values.getOrPut(input1 to input2) { function(input1, input2) }
}
}
}
/**
* A class that memoizes the result of a function. This method is threadsafe. Only one thread will
* ever invoke the backing function, other threads will block until a result is available, and then
* return that result.
*/
private class Memoize3(
private val function: (Input1, Input2, Input3) -> Result
) : (Input1, Input2, Input3) -> Result {
private val values = mutableMapOf, Result>()
private val locks = mutableMapOf, Any>()
@Synchronized
private fun getLock(input1: Input1, input2: Input2, input3: Input3): Any =
locks.getOrPut(Triple(input1, input2, input3)) { Object() }
override fun invoke(input1: Input1, input2: Input2, input3: Input3): Result {
val lock = getLock(input1, input2, input3)
return synchronized(lock) {
values.getOrPut(Triple(input1, input2, input3)) { function(input1, input2, input3) }
}
}
}
/**
* A class that memoizes the result of a function. This method is threadsafe. Only one thread will
* ever invoke the backing function, other threads will block until a result is available, and then
* return that result. The result will expire after the defined timeout, at which point the function
* can be executed again.
*/
private class MemoizeExpiring0(
private val validFor: Duration,
private val function: () -> Result,
) : () -> Result {
@Volatile private var value: Any? = UninitializedValue
private var expiration: ClockMark? = null
@Synchronized
override fun invoke(): Result {
if (value == UninitializedValue || expiration?.hasPassed() != false) {
value = function()
expiration = Clock.markNow() + validFor
}
@Suppress("UNCHECKED_CAST") return (value as Result)
}
}
/**
* A class that memoizes the result of a function. This method is threadsafe. Only one thread will
* ever invoke the backing function, other threads will block until a result is available, and then
* return that result. The result will expire after the defined timeout, at which point the function
* can be executed again.
*/
private class MemoizeExpiring1(
private val validFor: Duration,
private val function: (Input) -> Result,
) : (Input) -> Result {
private val values = mutableMapOf>()
private val locks = mutableMapOf()
@Synchronized
private fun getLock(input: Input): Any = locks.getOrPut(input) { Object() }
override fun invoke(input: Input): Result {
val lock = getLock(input)
return synchronized(lock) {
val (result, expiration) = values[input] ?: UninitializedValue to null
if (result == UninitializedValue || expiration?.hasPassed() != false) {
val computedResult = function(input)
values[input] = computedResult to Clock.markNow() + validFor
computedResult
} else {
@Suppress("UNCHECKED_CAST") (result as Result)
}
}
}
}
/**
* A class that memoizes the result of a function. This method is threadsafe. Only one thread will
* ever invoke the backing function, other threads will block until a result is available, and then
* return that result. The result will expire after the defined timeout, at which point the function
* can be executed again.
*/
private class MemoizeExpiring2(
private val validFor: Duration,
private val function: (Input1, Input2) -> Result,
) : (Input1, Input2) -> Result {
private val values = mutableMapOf, Pair>()
private val locks = mutableMapOf, Any>()
@Synchronized
private fun getLock(input1: Input1, input2: Input2): Any = locks.getOrPut(input1 to input2) { Object() }
override fun invoke(input1: Input1, input2: Input2): Result {
val lock = getLock(input1, input2)
return synchronized(lock) {
val (result, expiration) = values[input1 to input2] ?: UninitializedValue to null
if (result == UninitializedValue || expiration?.hasPassed() != false) {
val computedResult = function(input1, input2)
values[input1 to input2] = computedResult to Clock.markNow() + validFor
computedResult
} else {
@Suppress("UNCHECKED_CAST") (result as Result)
}
}
}
}
/**
* A class that memoizes the result of a function. This method is threadsafe. Only one thread will
* ever invoke the backing function, other threads will block until a result is available, and then
* return that result. The result will expire after the defined timeout, at which point the function
* can be executed again.
*/
private class MemoizeExpiring3(
private val validFor: Duration,
private val function: (Input1, Input2, Input3) -> Result,
) : (Input1, Input2, Input3) -> Result {
private val values = mutableMapOf, Pair>()
private val locks = mutableMapOf, Any>()
@Synchronized
private fun getLock(input1: Input1, input2: Input2, input3: Input3): Any = locks.getOrPut(Triple(input1, input2, input3)) { Object() }
override fun invoke(input1: Input1, input2: Input2, input3: Input3): Result {
val lock = getLock(input1, input2, input3)
return synchronized(lock) {
val (result, expiration) = values[Triple(input1, input2, input3)] ?: UninitializedValue to null
if (result == UninitializedValue || expiration?.hasPassed() != false) {
val computedResult = function(input1, input2, input3)
values[Triple(input1, input2, input3)] = computedResult to Clock.markNow() + validFor
computedResult
} else {
@Suppress("UNCHECKED_CAST") (result as Result)
}
}
}
}
/**
* Cache the result from calling this method. Subsequent calls, even with different parameters, will
* not change the cached output.
*
* TODO: use contracts when they're no longer experimental
*/
private class CachedFirstResultSuspend1(
private val f: suspend (Input) -> Result
) {
// contract { callsInPlace(f, EXACTLY_ONCE) }
private val initializeMutex = Mutex()
private object UNINITIALIZED_VALUE
@Volatile private var value: Any? = UNINITIALIZED_VALUE
fun cacheFirstResult(): suspend (Input) -> Result = { input ->
initializeMutex.withLock {
if (value == UNINITIALIZED_VALUE) {
value = f(input)
}
@Suppress("UNCHECKED_CAST") (value as Result)
}
}
}
/**
* Cache the result from calling this method. Subsequent calls, even with different parameters, will
* not change the cached output.
*
* TODO: use contracts when they're no longer experimental
*/
private class CachedFirstResultSuspend2(
private val f: suspend (Input1, Input2) -> Result
) {
// contract { callsInPlace(f, EXACTLY_ONCE) }
private val initializeMutex = Mutex()
private object UNINITIALIZED_VALUE
@Volatile private var value: Any? = UNINITIALIZED_VALUE
fun cacheFirstResult(): suspend (Input1, Input2) -> Result = { input1, input2 ->
initializeMutex.withLock {
if (value == UNINITIALIZED_VALUE) {
value = f(input1, input2)
}
@Suppress("UNCHECKED_CAST") (value as Result)
}
}
}
/**
* Cache the result from calling this method. Subsequent calls, even with different parameters, will
* not change the cached output.
*
* TODO: use contracts when they're no longer experimental
*/
private class CachedFirstResultSuspend3