├── .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-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_launch.xml │ │ └── activity_single_demo.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── getbouncer │ └── cardscan │ └── demo │ └── ExampleUnitTest.kt ├── cardscan-ui ├── .gitignore ├── README.md ├── build.gradle ├── cardscan-ui.iml ├── 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 ├── docs └── images │ └── demo.gif ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.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 │ └── res │ │ └── drawable │ │ └── ocr_card_numbers_clear.png │ ├── 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-full ├── .gitignore ├── build.gradle ├── deploy.gradle └── src │ ├── androidTest │ ├── java │ │ └── com │ │ │ └── getbouncer │ │ │ └── scan │ │ │ └── payment │ │ │ └── ml │ │ │ ├── CardDetectTest.kt │ │ │ └── SSDOcrTest.kt │ └── res │ │ └── drawable │ │ ├── card_no_card.png │ │ ├── card_no_pan.png │ │ ├── card_pan.png │ │ ├── ocr_card_numbers.png │ │ └── ocr_card_numbers_qr.png │ └── 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 │ └── res │ │ └── drawable │ │ ├── card_no_card.png │ │ ├── card_no_pan.png │ │ ├── card_pan.png │ │ ├── ocr_card_numbers.png │ │ └── ocr_card_numbers_qr.png │ └── main │ ├── AndroidManifest.xml │ └── assets │ ├── UX.0.25.106.8.tflite │ └── mb2_brex_metal_synthetic_svhnextra_epoch_3_5_98_8.tflite ├── 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 │ └── res │ │ ├── drawable │ │ ├── card_no_card.png │ │ ├── card_no_pan.png │ │ ├── card_pan.png │ │ ├── ocr_card_numbers.png │ │ └── ocr_card_numbers_qr.png │ │ └── raw │ │ └── sample_bitmap.bin │ ├── 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 │ └── res │ │ └── raw │ │ └── payment_card_types.bin │ └── test │ └── java │ └── com │ └── getbouncer │ └── scan │ └── payment │ └── card │ └── PaymentCardTest.kt ├── scan-ui ├── .gitignore ├── README.md ├── build.gradle ├── consumer-rules.pro ├── deploy.gradle ├── proguard-rules.pro ├── scan-ui.iml └── 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.gradle ├── settings └── checkstyle.xml ├── tensorflow-lite-arm-only ├── .gitignore ├── build.gradle ├── deploy.gradle └── tensorflow-lite-all-models-arm-only.aar └── tensorflow-lite ├── .gitignore ├── build.gradle ├── deploy.gradle └── tensorflow-lite-all-models.aar /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gradle 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: androidx.camera:camera-lifecycle 10 | versions: 11 | - 1.1.0-alpha04 12 | - dependency-name: androidx.camera:camera-camera2 13 | versions: 14 | - 1.1.0-alpha04 15 | - dependency-name: androidx.camera:camera-view 16 | versions: 17 | - 1.0.0-alpha24 18 | - dependency-name: androidx.camera:camera-core 19 | versions: 20 | - 1.1.0-alpha04 21 | - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-android 22 | versions: 23 | - 1.4.3-native-mt 24 | - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-core 25 | versions: 26 | - 1.4.3-native-mt 27 | - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-test 28 | versions: 29 | - 1.4.3-native-mt 30 | - dependency-name: org.jetbrains.kotlin:kotlin-test 31 | versions: 32 | - 1.4.31 33 | - dependency-name: org.jetbrains.kotlin.plugin.serialization 34 | versions: 35 | - 1.4.30 36 | - 1.4.31 37 | - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin 38 | versions: 39 | - 1.4.30 40 | - 1.4.31 41 | -------------------------------------------------------------------------------- /.github/workflows/android_test.yml: -------------------------------------------------------------------------------- 1 | name: Instrumentation Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | instrumentation-test: 11 | 12 | runs-on: macOS-latest 13 | 14 | steps: 15 | - name: checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: test 19 | uses: reactivecircus/android-emulator-runner@v2 20 | with: 21 | api-level: 29 22 | script: ./gradlew connectedCheck 23 | 24 | - name: upload-artifacts 25 | uses: actions/upload-artifact@v2 26 | if: failure() 27 | with: 28 | name: test-report 29 | path: ${{ github.workspace }}/*/build/reports/ 30 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | lint: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: set up JDK 1.8 19 | uses: actions/setup-java@v1 20 | with: 21 | java-version: 1.8 22 | 23 | - name: lint 24 | run: ./gradlew ktlint 25 | 26 | - name: CheckStyle 27 | run: ./gradlew checkJavaStyle 28 | -------------------------------------------------------------------------------- /.github/workflows/unit_test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: set up JDK 1.8 19 | uses: actions/setup-java@v1 20 | with: 21 | java-version: 1.8 22 | 23 | - name: test 24 | run: ./gradlew test 25 | 26 | - name: upload-artifacts 27 | uses: actions/upload-artifact@v2 28 | if: failure() 29 | with: 30 | name: test-report 31 | path: ${{ github.workspace }}/*/build/reports/ 32 | -------------------------------------------------------------------------------- /.github_changelog_generator: -------------------------------------------------------------------------------- 1 | since-tag=2.0.0015 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/compiler.xml 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | /.idea/encodings.xml 12 | /.idea/gradle.xml 13 | /.idea/misc.xml 14 | /.idea/runConfigurations.xml 15 | /.idea/vcs.xml 16 | /.idea/dictionaries 17 | /.idea/codeStyles/Project.xml 18 | /.idea/codeStyles/codeStyleConfig.xml 19 | /.idea/.name 20 | /.idea/checkstyle-idea.xml 21 | /.idea/jarRepositories.xml 22 | .DS_Store 23 | /build 24 | /captures 25 | .externalNativeBuild 26 | langapiconfig.json 27 | *.cxx 28 | github_token 29 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in 5 | # the repo. Unless a later match takes precedence, 6 | # @global-owner1 and @global-owner2 will be requested for 7 | # review when someone opens a pull request. 8 | * @awushensky @xsl @kingst @dxaen @awushensky-stripe 9 | 10 | # Order is important; the last matching pattern takes the most 11 | # precedence. When someone opens a pull request that only 12 | # modifies JS files, only @js-owner and not the global 13 | # owner(s) will be requested for a review. 14 | # *.js @js-owner 15 | 16 | # You can also use email addresses if you prefer. They'll be 17 | # used to look up users just like we do for commit author 18 | # emails. 19 | # *.go docs@example.com 20 | 21 | # In this example, @doctocat owns any files in the build/logs 22 | # directory at the root of the repository and any of its 23 | # subdirectories. 24 | # /build/logs/ @doctocat 25 | 26 | # The `docs/*` pattern will match files like 27 | # `docs/getting-started.md` but not further nested files like 28 | # `docs/build-app/troubleshooting.md`. 29 | # docs/* docs@example.com 30 | 31 | # In this example, @octocat owns any file in an apps directory 32 | # anywhere in your repository. 33 | # apps/ @octocat 34 | 35 | # In this example, @doctocat owns any file in the `/docs` 36 | # directory in the root of your repository. 37 | # /docs/ @doctocat 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011- Stripe, Inc. (https://stripe.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /cardscan-demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /cardscan-demo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion 30 6 | buildToolsVersion '30.0.3' 7 | 8 | defaultConfig { 9 | applicationId "com.getbouncer.cardscan.demo" 10 | minSdkVersion 21 11 | targetSdkVersion 30 12 | versionCode 1 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled true 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | 29 | kotlinOptions { 30 | jvmTarget = JavaVersion.VERSION_1_8.toString() 31 | } 32 | 33 | lintOptions { 34 | enable "Interoperability" 35 | } 36 | } 37 | 38 | dependencies { 39 | implementation fileTree(dir: 'libs', include: ['*.jar']) 40 | 41 | implementation project(":scan-camerax") 42 | implementation project(':scan-payment-full') 43 | implementation project(":cardscan-ui") 44 | 45 | implementation "androidx.appcompat:appcompat:1.3.1" 46 | implementation "androidx.core:core-ktx:1.6.0" 47 | implementation "androidx.constraintlayout:constraintlayout:2.1.0" 48 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1" 49 | } 50 | 51 | dependencies { 52 | testImplementation "androidx.test:core:1.4.0" 53 | testImplementation "androidx.test:runner:1.4.0" 54 | testImplementation "junit:junit:4.13.2" 55 | testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 56 | 57 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' 58 | } 59 | 60 | dependencies { 61 | androidTestImplementation "androidx.test.ext:junit:1.1.3" 62 | androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" 63 | androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 64 | } 65 | -------------------------------------------------------------------------------- /cardscan-demo/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /cardscan-demo/src/androidTest/java/com/getbouncer/cardscan/demo/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.cardscan.demo 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import org.junit.Test 5 | import kotlin.test.assertEquals 6 | 7 | /** 8 | * Instrumented test, which will execute on an Android device. 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleInstrumentedTest { 13 | @Test 14 | fun useAppContext() { 15 | // Context of the app under test. 16 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 17 | assertEquals("com.getbouncer.cardscan.demo", appContext.packageName) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cardscan-demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/cardscan-demo/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/cardscan-demo/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/cardscan-demo/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/cardscan-demo/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/cardscan-demo/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/cardscan-demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/cardscan-demo/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/cardscan-demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/cardscan-demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/cardscan-demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CardScan Demo 3 | 4 | Scan Card 5 | 6 | User pressed back 7 | Scan canceled 8 | Enter card manually 9 | Camera permission denied 10 | Single Activity Demo 11 | 12 | Enable Debug 13 | Enable name extraction 14 | Enable expiry extraction 15 | Enable manual entry 16 | 17 | Device architecture is %1$s 18 | 19 | -------------------------------------------------------------------------------- /cardscan-demo/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /cardscan-demo/src/test/java/com/getbouncer/cardscan/demo/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.cardscan.demo 2 | 3 | import org.junit.Test 4 | import kotlin.test.assertEquals 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cardscan-ui/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /cardscan-ui/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-parcelize' 4 | 5 | android { 6 | compileSdkVersion 30 7 | buildToolsVersion '30.0.3' 8 | 9 | defaultConfig { 10 | minSdkVersion 21 11 | targetSdkVersion 30 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles 'consumer-rules.pro' 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | testOptions { 25 | unitTests.includeAndroidResources = true 26 | } 27 | 28 | lintOptions { 29 | enable "Interoperability" 30 | } 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = JavaVersion.VERSION_1_8.toString() 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation fileTree(dir: 'libs', include: ['*.jar']) 44 | api project(":scan-framework") 45 | api project(':scan-camera') 46 | api project(":scan-payment") 47 | api project(":scan-ui") 48 | 49 | implementation "androidx.appcompat:appcompat:[1.3.0,1.3.1]" 50 | implementation "androidx.core:core-ktx:[1.3.1,1.6.0]" 51 | implementation 'androidx.constraintlayout:constraintlayout:[2.0.4,2.1.0]' 52 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:[1.4.0,1.5.1]" 53 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:[1.1.0,1.2.2]" 54 | } 55 | 56 | dependencies { 57 | testImplementation "androidx.test:core:1.4.0" 58 | testImplementation "androidx.test:runner:1.4.0" 59 | testImplementation "junit:junit:4.13.2" 60 | testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 61 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1" 62 | } 63 | 64 | dependencies { 65 | androidTestImplementation "androidx.test.ext:junit:1.1.3" 66 | androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" 67 | androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 68 | } 69 | 70 | apply from: 'deploy.gradle' 71 | -------------------------------------------------------------------------------- /cardscan-ui/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/cardscan-ui/consumer-rules.pro -------------------------------------------------------------------------------- /cardscan-ui/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /cardscan-ui/src/androidTest/java/com/getbouncer/cardscan/ui/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.cardscan.ui 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import org.junit.Test 5 | import kotlin.test.assertEquals 6 | 7 | class ExampleInstrumentedTest { 8 | @Test 9 | fun useAppContext() { 10 | // Context of the app under test. 11 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 12 | assertEquals("com.getbouncer.cardscan.ui.test", appContext.packageName) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cardscan-ui/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/analyzer/CompletionLoopAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.cardscan.ui.analyzer 2 | 3 | import com.getbouncer.cardscan.ui.SavedFrame 4 | import com.getbouncer.scan.framework.Analyzer 5 | import com.getbouncer.scan.framework.AnalyzerFactory 6 | import com.getbouncer.scan.payment.analyzer.NameAndExpiryAnalyzer 7 | 8 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 9 | class CompletionLoopAnalyzer private constructor( 10 | private val nameAndExpiryAnalyzer: NameAndExpiryAnalyzer?, 11 | ) : Analyzer { 12 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 13 | class Prediction( 14 | val nameAndExpiryResult: NameAndExpiryAnalyzer.Prediction?, 15 | val isNameExtractionAvailable: Boolean, 16 | val isExpiryExtractionAvailable: Boolean, 17 | val enableNameExtraction: Boolean, 18 | val enableExpiryExtraction: Boolean, 19 | ) 20 | 21 | override suspend fun analyze( 22 | data: SavedFrame, 23 | state: Unit, 24 | ) = Prediction( 25 | nameAndExpiryResult = nameAndExpiryAnalyzer?.analyze( 26 | NameAndExpiryAnalyzer.Input(data.frame.cameraPreviewImage.image, data.frame.cameraPreviewImage.previewImageBounds, data.frame.cardFinder), 27 | state, 28 | ), 29 | isNameExtractionAvailable = nameAndExpiryAnalyzer?.isNameDetectorAvailable() ?: false, 30 | isExpiryExtractionAvailable = nameAndExpiryAnalyzer?.isExpiryDetectorAvailable() ?: false, 31 | enableNameExtraction = nameAndExpiryAnalyzer?.runNameExtraction ?: false, 32 | enableExpiryExtraction = nameAndExpiryAnalyzer?.runExpiryExtraction ?: false, 33 | ) 34 | 35 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 36 | class Factory( 37 | private val nameAndExpiryFactory: AnalyzerFactory, 38 | ) : AnalyzerFactory { 39 | override suspend fun newInstance() = CompletionLoopAnalyzer( 40 | nameAndExpiryAnalyzer = nameAndExpiryFactory.newInstance(), 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/analyzer/MainLoopAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.cardscan.ui.analyzer 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Rect 5 | import com.getbouncer.cardscan.ui.result.MainLoopState 6 | import com.getbouncer.scan.camera.CameraPreviewImage 7 | import com.getbouncer.scan.framework.Analyzer 8 | import com.getbouncer.scan.framework.AnalyzerFactory 9 | import com.getbouncer.scan.payment.ml.CardDetect 10 | import com.getbouncer.scan.payment.ml.SSDOcr 11 | 12 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 13 | class MainLoopAnalyzer( 14 | private val ssdOcr: Analyzer?, 15 | private val cardDetect: Analyzer?, 16 | ) : Analyzer { 17 | 18 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 19 | data class Input( 20 | val cameraPreviewImage: CameraPreviewImage, 21 | val cardFinder: Rect, 22 | ) 23 | 24 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 25 | class Prediction( 26 | val ocr: SSDOcr.Prediction?, 27 | val card: CardDetect.Prediction?, 28 | ) { 29 | val isCardVisible = card?.side?.let { it == CardDetect.Prediction.Side.NO_PAN || it == CardDetect.Prediction.Side.PAN } 30 | } 31 | 32 | override suspend fun analyze(data: Input, state: MainLoopState): Prediction { 33 | val cardResult = if (state.runCardDetect) cardDetect?.analyze(CardDetect.cameraPreviewToInput(data.cameraPreviewImage.image, data.cameraPreviewImage.previewImageBounds, data.cardFinder), Unit) else null 34 | val ocrResult = if (state.runOcr) ssdOcr?.analyze(SSDOcr.cameraPreviewToInput(data.cameraPreviewImage.image, data.cameraPreviewImage.previewImageBounds, data.cardFinder), Unit) else null 35 | 36 | return Prediction( 37 | ocr = ocrResult, 38 | card = cardResult, 39 | ) 40 | } 41 | 42 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 43 | class Factory( 44 | private val ssdOcrFactory: AnalyzerFactory>, 45 | private val cardDetectFactory: AnalyzerFactory>, 46 | ) : AnalyzerFactory { 47 | override suspend fun newInstance(): MainLoopAnalyzer = MainLoopAnalyzer( 48 | ssdOcr = ssdOcrFactory.newInstance(), 49 | cardDetect = cardDetectFactory.newInstance(), 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/exception/StripeNetworkException.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.cardscan.ui.exception 2 | 3 | import java.lang.Exception 4 | 5 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 6 | class StripeNetworkException(message: String) : Exception(message) 7 | -------------------------------------------------------------------------------- /cardscan-ui/src/main/java/com/getbouncer/cardscan/ui/exception/UnknownScanException.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.cardscan.ui.exception 2 | 3 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 4 | class UnknownScanException(message: String? = null) : Exception(message) 5 | -------------------------------------------------------------------------------- /cardscan-ui/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #AA000000 4 | @android:color/white 5 | 6 | @android:color/white 7 | @android:color/black 8 | 9 | -------------------------------------------------------------------------------- /cardscan-ui/src/main/res/values/dimensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24sp 4 | 5 | 24dp 6 | 7 | 16dp 8 | 18sp 9 | 10 | -------------------------------------------------------------------------------- /cardscan-ui/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Processing, please wait 4 | 5 | Enter card manually 6 | 7 | -------------------------------------------------------------------------------- /docs/images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/docs/images/demo.gif -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | version=2.2.0003 23 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Feb 26 15:43:08 PST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /scan-camera/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /scan-camera/README.md: -------------------------------------------------------------------------------- 1 | # Deprecation Notice 2 | Hello from the Stripe (formerly Bouncer) team! 3 | 4 | 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. 5 | 6 | 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! 7 | 8 | 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. 9 | 10 | 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). 11 | 12 | For the new product, please visit the [stripe github repository](https://github.com/stripe/stripe-android/tree/master/stripecardscan). 13 | 14 | # Overview 15 | 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. 16 | 17 | 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. 18 | 19 | ![demo](../docs/images/demo.gif) 20 | 21 | ## Contents 22 | * [Requirements](#requirements) 23 | * [Demo](#demo) 24 | * [Integration](#integration) 25 | * [Using](#using) 26 | * [Developing](#developing) 27 | * [Authors](#authors) 28 | * [License](#license) 29 | 30 | ## Requirements 31 | * Android API level 21 or higher 32 | * Kotlin coroutine compatibility 33 | 34 | Note: Your app does not have to be written in kotlin to integrate scan-camera, but must be able to depend on kotlin functionality. 35 | 36 | ## Demo 37 | An app demonstrating the basic capabilities of CardScan is available in [github](https://github.com/getbouncer/cardscan-demo-android). 38 | 39 | ## Integration 40 | See the [integration documentation](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) in the Bouncer Docs. 41 | 42 | ## Using 43 | 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. 44 | 45 | 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). 46 | 47 | ### Getting images from the camera 48 | See the [example code](https://docs.getbouncer.com/card-scan/android-integration-guide/android-architecture-overview#example) in the Android architecture documentation. 49 | 50 | ## Developing 51 | See the [development docs](https://docs.getbouncer.com/card-scan/android-integration-guide/android-development-guide) for details on developing this library. 52 | 53 | ## Authors 54 | Adam Wushensky, Sam King, and Zain ul Abi Din 55 | 56 | ## License 57 | This library is available under the MIT license. See the [LICENSE](../LICENSE) file for the full license text. 58 | -------------------------------------------------------------------------------- /scan-camera/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion 30 6 | buildToolsVersion '30.0.3' 7 | 8 | defaultConfig { 9 | minSdkVersion 21 10 | targetSdkVersion 30 11 | 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | consumerProguardFiles 'consumer-rules.pro' 14 | } 15 | 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | 23 | testOptions { 24 | unitTests.includeAndroidResources = true 25 | } 26 | 27 | lintOptions { 28 | enable "Interoperability" 29 | } 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | } 36 | 37 | dependencies { 38 | implementation fileTree(dir: 'libs', include: ['*.jar']) 39 | implementation project(":scan-framework") 40 | 41 | implementation "androidx.appcompat:appcompat:[1.3.0,1.3.1]" 42 | implementation "androidx.core:core-ktx:[1.3.1,1.6.0]" 43 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:[1.4.0,1.5.1]" 44 | } 45 | 46 | dependencies { 47 | testImplementation "androidx.test:core:1.4.0" 48 | testImplementation "androidx.test:runner:1.4.0" 49 | testImplementation "junit:junit:4.13.2" 50 | testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 51 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1" 52 | } 53 | 54 | dependencies { 55 | androidTestImplementation "androidx.test.ext:junit:1.1.3" 56 | androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" 57 | androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 58 | } 59 | 60 | apply from: 'deploy.gradle' 61 | -------------------------------------------------------------------------------- /scan-camera/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-camera/consumer-rules.pro -------------------------------------------------------------------------------- /scan-camera/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /scan-camera/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /scan-camera/src/main/java/com/getbouncer/scan/camera/CameraSelector.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.camera 2 | 3 | import android.app.Activity 4 | import android.graphics.Bitmap 5 | import android.os.Build 6 | import android.util.Log 7 | import android.util.Size 8 | import android.view.ViewGroup 9 | import com.getbouncer.scan.framework.Config 10 | 11 | /** 12 | * Get the appropriate camera adapter. If the customer has provided an additional camera adapter, use that in place of 13 | * camera 1. 14 | */ 15 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 16 | fun getCameraAdapter( 17 | activity: Activity, 18 | previewView: ViewGroup, 19 | minimumResolution: Size, 20 | cameraErrorListener: CameraErrorListener, 21 | ): CameraAdapter> = 22 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { 23 | try { 24 | getAlternateCamera(activity, previewView, minimumResolution, cameraErrorListener) 25 | } catch (t: Throwable) { 26 | Log.d(Config.logTag, "No alternative camera implementations supplied, falling back to default", t) 27 | Camera1Adapter(activity, previewView, minimumResolution, cameraErrorListener) 28 | } 29 | } else { 30 | Log.d(Config.logTag, "YUV_420_888 is not supported, falling back to default implementation") 31 | Camera1Adapter(activity, previewView, minimumResolution, cameraErrorListener) 32 | }.apply { 33 | Log.d(Config.logTag, "Using camera implementation ${this.implementationName}") 34 | } 35 | 36 | @Suppress("UNCHECKED_CAST") 37 | @Throws(ClassNotFoundException::class, NoSuchMethodException::class, IllegalAccessException::class) 38 | private fun getAlternateCamera( 39 | activity: Activity, 40 | previewView: ViewGroup, 41 | minimumResolution: Size, 42 | cameraErrorListener: CameraErrorListener, 43 | ): CameraAdapter> = 44 | Class.forName("com.getbouncer.scan.camera.extension.CameraAdapterImpl") 45 | .getConstructor( 46 | Activity::class.java, 47 | ViewGroup::class.java, 48 | Size::class.java, 49 | CameraErrorListener::class.java, 50 | ) 51 | .newInstance( 52 | activity, 53 | previewView, 54 | minimumResolution, 55 | cameraErrorListener, 56 | ) as CameraAdapter> 57 | -------------------------------------------------------------------------------- /scan-camera2/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /scan-camera2/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion 30 6 | buildToolsVersion '30.0.3' 7 | 8 | defaultConfig { 9 | minSdkVersion 21 10 | targetSdkVersion 30 11 | 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | consumerProguardFiles 'consumer-rules.pro' 14 | } 15 | 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | 23 | testOptions { 24 | unitTests.includeAndroidResources = true 25 | } 26 | 27 | lintOptions { 28 | enable "Interoperability" 29 | } 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | } 36 | 37 | dependencies { 38 | implementation fileTree(dir: 'libs', include: ['*.jar']) 39 | implementation project(":scan-framework") 40 | implementation project(':scan-camera') 41 | 42 | implementation "androidx.appcompat:appcompat:[1.3.0,1.3.1]" 43 | implementation "androidx.core:core-ktx:[1.3.1,1.6.0]" 44 | } 45 | 46 | dependencies { 47 | testImplementation "androidx.test:core:1.4.0" 48 | testImplementation "androidx.test:runner:1.4.0" 49 | testImplementation "junit:junit:4.13.2" 50 | testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 51 | } 52 | 53 | dependencies { 54 | androidTestImplementation "androidx.test.ext:junit:1.1.3" 55 | androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" 56 | androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 57 | } 58 | 59 | apply from: 'deploy.gradle' 60 | -------------------------------------------------------------------------------- /scan-camera2/src/androidTest/java/com/getbouncer/scan/camera/extension/UtilInstrumentationTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.camera.extension 2 | 3 | import android.util.Size 4 | import android.view.Surface 5 | import androidx.test.filters.SmallTest 6 | import org.junit.Test 7 | import kotlin.test.assertEquals 8 | 9 | class UtilInstrumentationTest { 10 | 11 | @Test 12 | @SmallTest 13 | fun resolutionToSize_perpendicular() { 14 | assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 90)) 15 | assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 270)) 16 | assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 0)) 17 | assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 180)) 18 | assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 90)) 19 | assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 270)) 20 | assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 0)) 21 | assertEquals(Size(60, 120), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 180)) 22 | } 23 | 24 | @Test 25 | @SmallTest 26 | fun resolutionToSize_parallel() { 27 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 0)) 28 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 180)) 29 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 90)) 30 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 270)) 31 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 0)) 32 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 180)) 33 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 90)) 34 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 270)) 35 | } 36 | 37 | @Test 38 | @SmallTest 39 | fun resolutionToSize_oblique() { 40 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 45)) 41 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_0, 135)) 42 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 45)) 43 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_90, 135)) 44 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 45)) 45 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_180, 135)) 46 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 45)) 47 | assertEquals(Size(120, 60), Size(120, 60).resolutionToSize(Surface.ROTATION_270, 135)) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /scan-camera2/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /scan-camera2/src/main/java/com/getbouncer/scan/camera/extension/CameraDetails.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.camera.extension 2 | 3 | import android.hardware.camera2.params.StreamConfigurationMap 4 | 5 | /** 6 | * Details about a camera. 7 | */ 8 | internal data class CameraDetails( 9 | val cameraId: String, 10 | val flashAvailable: Boolean, 11 | val config: StreamConfigurationMap, 12 | val sensorRotation: Int, 13 | val supportedAutoFocusModes: List, 14 | val lensFacing: Int?, 15 | ) 16 | -------------------------------------------------------------------------------- /scan-camera2/src/test/java/com/getbouncer/scan/camera/extension/UtilTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.camera.extension 2 | 3 | import android.view.Surface 4 | import androidx.test.filters.SmallTest 5 | import org.junit.Test 6 | import kotlin.test.assertEquals 7 | 8 | class UtilTest { 9 | 10 | @Test 11 | @SmallTest 12 | fun calculateImageRotationDegrees_vertical() { 13 | assertEquals(0, calculateImageRotationDegrees(Surface.ROTATION_0, 0)) 14 | assertEquals(45, calculateImageRotationDegrees(Surface.ROTATION_0, 45)) 15 | assertEquals(315, calculateImageRotationDegrees(Surface.ROTATION_0, -45)) 16 | assertEquals(180, calculateImageRotationDegrees(Surface.ROTATION_0, 180)) 17 | assertEquals(180, calculateImageRotationDegrees(Surface.ROTATION_0, -180)) 18 | } 19 | 20 | @Test 21 | @SmallTest 22 | fun calculateImageRotationDegrees_right() { 23 | assertEquals(270, calculateImageRotationDegrees(Surface.ROTATION_90, 0)) 24 | assertEquals(315, calculateImageRotationDegrees(Surface.ROTATION_90, 45)) 25 | assertEquals(225, calculateImageRotationDegrees(Surface.ROTATION_90, -45)) 26 | assertEquals(90, calculateImageRotationDegrees(Surface.ROTATION_90, 180)) 27 | assertEquals(90, calculateImageRotationDegrees(Surface.ROTATION_90, -180)) 28 | } 29 | 30 | @Test 31 | @SmallTest 32 | fun calculateImageRotationDegrees_left() { 33 | assertEquals(90, calculateImageRotationDegrees(Surface.ROTATION_270, 0)) 34 | assertEquals(135, calculateImageRotationDegrees(Surface.ROTATION_270, 45)) 35 | assertEquals(45, calculateImageRotationDegrees(Surface.ROTATION_270, -45)) 36 | assertEquals(270, calculateImageRotationDegrees(Surface.ROTATION_270, 180)) 37 | assertEquals(270, calculateImageRotationDegrees(Surface.ROTATION_270, -180)) 38 | } 39 | 40 | @Test 41 | @SmallTest 42 | fun calculateImageRotationDegrees_inverted() { 43 | assertEquals(180, calculateImageRotationDegrees(Surface.ROTATION_180, 0)) 44 | assertEquals(225, calculateImageRotationDegrees(Surface.ROTATION_180, 45)) 45 | assertEquals(135, calculateImageRotationDegrees(Surface.ROTATION_180, -45)) 46 | assertEquals(0, calculateImageRotationDegrees(Surface.ROTATION_180, 180)) 47 | assertEquals(0, calculateImageRotationDegrees(Surface.ROTATION_180, -180)) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /scan-camerax/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /scan-camerax/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion 30 6 | buildToolsVersion '30.0.3' 7 | 8 | defaultConfig { 9 | minSdkVersion 21 10 | targetSdkVersion 30 11 | 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | consumerProguardFiles 'consumer-rules.pro' 14 | } 15 | 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | 23 | testOptions { 24 | unitTests.includeAndroidResources = true 25 | } 26 | 27 | lintOptions { 28 | enable "Interoperability" 29 | } 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = JavaVersion.VERSION_1_8.toString() 38 | } 39 | } 40 | 41 | dependencies { 42 | implementation fileTree(dir: 'libs', include: ['*.jar']) 43 | implementation project(":scan-framework") 44 | implementation project(':scan-camera') 45 | 46 | implementation "androidx.appcompat:appcompat:[1.3.0,1.3.1]" 47 | implementation "androidx.camera:camera-camera2:[1.0.0,1.0.1]" 48 | implementation "androidx.camera:camera-core:[1.0.0,1.0.1]" 49 | implementation "androidx.camera:camera-lifecycle:[1.0.0,1.0.1]" 50 | implementation "androidx.camera:camera-view:[1.0.0-alpha26,1.0.0-alpha28)" 51 | implementation "androidx.core:core-ktx:[1.3.1,1.6.0]" 52 | } 53 | 54 | dependencies { 55 | testImplementation "androidx.test:core:1.4.0" 56 | testImplementation "androidx.test:runner:1.4.0" 57 | testImplementation "junit:junit:4.13.2" 58 | testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 59 | } 60 | 61 | dependencies { 62 | androidTestImplementation "androidx.test.ext:junit:1.1.3" 63 | androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" 64 | androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 65 | } 66 | 67 | apply from: 'deploy.gradle' 68 | -------------------------------------------------------------------------------- /scan-camerax/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /scan-camerax/src/main/java/com/getbouncer/scan/camera/extension/Image.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.camera.extension 2 | 3 | import android.graphics.ImageFormat 4 | import android.renderscript.RenderScript 5 | import androidx.annotation.CheckResult 6 | import androidx.camera.core.ImageProxy 7 | import com.getbouncer.scan.framework.exception.ImageTypeNotSupportedException 8 | import com.getbouncer.scan.framework.image.NV21Image 9 | import com.getbouncer.scan.framework.image.yuvPlanesToNV21Fast 10 | import com.getbouncer.scan.framework.util.mapArray 11 | import com.getbouncer.scan.framework.util.mapToIntArray 12 | import com.getbouncer.scan.framework.util.toByteArray 13 | 14 | /** 15 | * Convert an ImageProxy to a bitmap. 16 | */ 17 | @CheckResult 18 | internal fun ImageProxy.toBitmap(renderScript: RenderScript) = when (format) { 19 | ImageFormat.NV21 -> NV21Image(width, height, planes[0].buffer.toByteArray()).toBitmap(renderScript) 20 | ImageFormat.YUV_420_888 -> NV21Image( 21 | width, 22 | height, 23 | yuvPlanesToNV21Fast( 24 | width, 25 | height, 26 | planes.mapArray { it.buffer }, 27 | planes.mapToIntArray { it.rowStride }, 28 | planes.mapToIntArray { it.pixelStride }, 29 | ), 30 | ).toBitmap(renderScript) 31 | else -> throw ImageTypeNotSupportedException(format) 32 | } 33 | -------------------------------------------------------------------------------- /scan-camerax/src/main/java/com/getbouncer/scan/camera/extension/Util.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.camera.extension 2 | 3 | import android.util.Size 4 | import kotlin.math.max 5 | import kotlin.math.min 6 | 7 | /** 8 | * Convert a resolution to a size on the screen based only on the display size. 9 | */ 10 | internal fun Size.resolutionToSize(displaySize: Size) = when { 11 | displaySize.width >= displaySize.height -> Size( 12 | /* width */ 13 | max(width, height), 14 | /* height */ 15 | min(width, height), 16 | ) 17 | else -> Size( 18 | /* width */ 19 | min(width, height), 20 | /* height */ 21 | max(width, height), 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /scan-framework/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /scan-framework/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlinx-serialization' 4 | 5 | android { 6 | compileSdkVersion 30 7 | buildToolsVersion '30.0.3' 8 | 9 | defaultConfig { 10 | minSdkVersion 21 11 | targetSdkVersion 30 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles "consumer-rules.pro" 15 | 16 | buildConfigField("String", "SDK_VERSION_STRING", "\"${version}\"") 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 23 | } 24 | } 25 | 26 | testOptions { 27 | unitTests.includeAndroidResources = true 28 | } 29 | 30 | lintOptions { 31 | enable "Interoperability" 32 | } 33 | 34 | packagingOptions { 35 | pickFirst 'META-INF/AL2.0' 36 | pickFirst 'META-INF/LGPL2.1' 37 | } 38 | 39 | compileOptions { 40 | sourceCompatibility JavaVersion.VERSION_1_8 41 | targetCompatibility JavaVersion.VERSION_1_8 42 | } 43 | 44 | kotlinOptions { 45 | jvmTarget = JavaVersion.VERSION_1_8.toString() 46 | } 47 | 48 | aaptOptions { 49 | noCompress "tflite" 50 | } 51 | } 52 | 53 | dependencies { 54 | implementation fileTree(dir: "libs", include: ["*.jar"]) 55 | 56 | implementation "androidx.core:core-ktx:[1.3.1,1.6.0]" 57 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:[1.4.0,1.5.1]" 58 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:[1.4.0,1.5.1]" 59 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:[1.1.0,1.2.2]" 60 | 61 | // Allow the user to specify their own version of Tensorflow Lite to include 62 | runtimeOnly project(":tensorflow-lite") 63 | compileOnly "org.tensorflow:tensorflow-lite:2.4.0" 64 | } 65 | 66 | dependencies { 67 | testImplementation "androidx.test:core:1.4.0" 68 | testImplementation "androidx.test:runner:1.4.0" 69 | testImplementation "junit:junit:4.13.2" 70 | testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 71 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1" 72 | } 73 | 74 | dependencies { 75 | androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" 76 | androidTestImplementation "androidx.test.ext:junit:1.1.3" 77 | androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 78 | androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1" 79 | } 80 | 81 | apply from: "deploy.gradle" 82 | -------------------------------------------------------------------------------- /scan-framework/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-framework/consumer-rules.pro -------------------------------------------------------------------------------- /scan-framework/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keep class org.tensorflow.lite.Interpreter { *; } 24 | -------------------------------------------------------------------------------- /scan-framework/src/androidTest/assets/sample_resource.tflite: -------------------------------------------------------------------------------- 1 | ABC123 2 | DEF456 3 | -------------------------------------------------------------------------------- /scan-framework/src/androidTest/java/com/getbouncer/scan/framework/LoaderTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework 2 | 3 | import androidx.test.filters.SmallTest 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import kotlinx.coroutines.runBlocking 6 | import org.junit.Test 7 | import java.io.File 8 | import kotlin.test.assertEquals 9 | import kotlin.test.assertNotNull 10 | import kotlin.test.assertTrue 11 | 12 | class LoaderTest { 13 | private val testContext = InstrumentationRegistry.getInstrumentation().context 14 | 15 | @Test 16 | @SmallTest 17 | fun loadData_fromResource_success() = runBlocking { 18 | val fetchedData = FetchedResource( 19 | modelClass = "sample_class", 20 | modelFrameworkVersion = 2049, 21 | modelVersion = "sample_resource", 22 | modelHash = "0dcf3e387c68dfea8dd72a183f1f765478ebaa4d8544cfc09a16e87a795d8ccf", 23 | modelHashAlgorithm = "SHA-256", 24 | assetFileName = "sample_resource.tflite", 25 | ) 26 | 27 | val byteBuffer = Loader(testContext).loadData(fetchedData) 28 | assertNotNull(byteBuffer) 29 | assertEquals(14, byteBuffer.limit(), "File is not expected size") 30 | byteBuffer.rewind() 31 | 32 | // ensure not all bytes are zero 33 | var encounteredNonZeroByte = false 34 | while (!encounteredNonZeroByte) { 35 | encounteredNonZeroByte = byteBuffer.get().toInt() != 0 36 | } 37 | assertTrue(encounteredNonZeroByte, "All bytes were zero") 38 | 39 | // ensure bytes are correct 40 | byteBuffer.rewind() 41 | assertEquals('A', byteBuffer.get().toInt().toChar()) 42 | assertEquals('B', byteBuffer.get().toInt().toChar()) 43 | assertEquals('C', byteBuffer.get().toInt().toChar()) 44 | assertEquals('1', byteBuffer.get().toInt().toChar()) 45 | } 46 | 47 | @Test 48 | @SmallTest 49 | fun loadData_fromFile_success() = runBlocking { 50 | val sampleFile = File(testContext.cacheDir, "sample_file") 51 | if (sampleFile.exists()) { 52 | sampleFile.delete() 53 | } 54 | 55 | sampleFile.createNewFile() 56 | sampleFile.writeText("ABC123") 57 | 58 | val fetchedData = FetchedFile( 59 | modelClass = "sample_class", 60 | modelFrameworkVersion = 2049, 61 | modelVersion = "sample_file", 62 | modelHash = "133351546614bfadfa68bb66c22a06265972b02791e4ac545ad900f20fe1a796", 63 | modelHashAlgorithm = "SHA-256", 64 | file = sampleFile, 65 | ) 66 | 67 | val byteBuffer = Loader(testContext).loadData(fetchedData) 68 | assertNotNull(byteBuffer) 69 | assertEquals(6, byteBuffer.limit(), "File is not expected size") 70 | byteBuffer.rewind() 71 | 72 | // ensure not all bytes are zero 73 | var encounteredNonZeroByte = false 74 | while (!encounteredNonZeroByte) { 75 | encounteredNonZeroByte = byteBuffer.get().toInt() != 0 76 | } 77 | assertTrue(encounteredNonZeroByte, "All bytes were zero") 78 | 79 | // ensure bytes are correct 80 | byteBuffer.rewind() 81 | assertEquals('A', byteBuffer.get().toInt().toChar()) 82 | assertEquals('B', byteBuffer.get().toInt().toChar()) 83 | assertEquals('C', byteBuffer.get().toInt().toChar()) 84 | assertEquals('1', byteBuffer.get().toInt().toChar()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /scan-framework/src/androidTest/java/com/getbouncer/scan/framework/image/BitmapExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.image 2 | 3 | import android.graphics.Rect 4 | import android.util.Size 5 | import androidx.core.graphics.drawable.toBitmap 6 | import androidx.test.filters.SmallTest 7 | import androidx.test.platform.app.InstrumentationRegistry 8 | import com.getbouncer.scan.framework.test.R 9 | import org.junit.Test 10 | import kotlin.test.assertEquals 11 | import kotlin.test.assertNotNull 12 | import kotlin.test.assertTrue 13 | 14 | class BitmapExtensionsTest { 15 | 16 | private val testResources = InstrumentationRegistry.getInstrumentation().context.resources 17 | 18 | @Test 19 | @SmallTest 20 | fun bitmap_scale_isCorrect() { 21 | // read in a sample bitmap file 22 | val bitmap = testResources.getDrawable(R.drawable.ocr_card_numbers_clear, null).toBitmap() 23 | assertNotNull(bitmap) 24 | assertEquals(600, bitmap.width, "Bitmap width is not expected") 25 | assertEquals(375, bitmap.height, "Bitmap height is not expected") 26 | 27 | // scale the bitmap 28 | val scaledBitmap = bitmap.scale(0.2F) 29 | 30 | // check the expected sizes of the images 31 | assertEquals( 32 | Size(bitmap.width / 5, bitmap.height / 5), 33 | Size(scaledBitmap.width, scaledBitmap.height), 34 | "Scaled image is the wrong size" 35 | ) 36 | 37 | // check each pixel of the images 38 | var encounteredNonZeroPixel = false 39 | for (x in 0 until scaledBitmap.width) { 40 | for (y in 0 until scaledBitmap.height) { 41 | encounteredNonZeroPixel = encounteredNonZeroPixel || scaledBitmap.getPixel(x, y) != 0 42 | } 43 | } 44 | 45 | assertTrue(encounteredNonZeroPixel, "Pixels were all zero") 46 | } 47 | 48 | @Test 49 | @SmallTest 50 | fun bitmap_crop_isCorrect() { 51 | val bitmap = testResources.getDrawable(R.drawable.ocr_card_numbers_clear, null).toBitmap() 52 | assertNotNull(bitmap) 53 | assertEquals(600, bitmap.width, "Bitmap width is not expected") 54 | assertEquals(375, bitmap.height, "Bitmap height is not expected") 55 | 56 | // crop the bitmap 57 | val croppedBitmap = bitmap.crop( 58 | Rect( 59 | bitmap.width / 4, 60 | bitmap.height / 4, 61 | bitmap.width * 3 / 4, 62 | bitmap.height * 3 / 4 63 | ) 64 | ) 65 | 66 | // check the expected sizes of the images 67 | assertEquals( 68 | Size(bitmap.width * 3 / 4 - bitmap.width / 4, bitmap.height * 3 / 4 - bitmap.height / 4), 69 | Size(croppedBitmap.width, croppedBitmap.height), 70 | "Cropped image is the wrong size" 71 | ) 72 | 73 | // check each pixel of the images 74 | var encounteredNonZeroPixel = false 75 | for (x in 0 until croppedBitmap.width) { 76 | for (y in 0 until croppedBitmap.height) { 77 | val croppedPixel = croppedBitmap.getPixel(x, y) 78 | val originalPixel = bitmap.getPixel(x + bitmap.width / 4, y + bitmap.height / 4) 79 | assertEquals(originalPixel, croppedPixel, "Difference at pixel $x, $y") 80 | encounteredNonZeroPixel = encounteredNonZeroPixel || croppedPixel != 0 81 | } 82 | } 83 | 84 | assertTrue(encounteredNonZeroPixel, "Pixels were all zero") 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /scan-framework/src/androidTest/java/com/getbouncer/scan/framework/util/AppDetailsTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.util 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import org.junit.Test 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertTrue 7 | 8 | class AppDetailsTest { 9 | private val testContext = InstrumentationRegistry.getInstrumentation().context 10 | 11 | @Test 12 | fun appDetails_full() { 13 | val appDetails = AppDetails.fromContext(testContext) 14 | 15 | assertEquals("com.getbouncer.scan.framework.test", appDetails.appPackageName) 16 | assertEquals("", appDetails.applicationId) 17 | assertEquals("com.getbouncer.scan.framework", appDetails.libraryPackageName) 18 | assertTrue(appDetails.sdkVersion.startsWith("2."), "${appDetails.sdkVersion} does not start with \"2.\"") 19 | assertEquals(-1, appDetails.sdkVersionCode) 20 | assertTrue(appDetails.sdkFlavor.isNotEmpty()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scan-framework/src/androidTest/res/drawable/ocr_card_numbers_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-framework/src/androidTest/res/drawable/ocr_card_numbers_clear.png -------------------------------------------------------------------------------- /scan-framework/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/Analyzer.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework 2 | 3 | import java.io.Closeable 4 | 5 | /** 6 | * The default number of analyzers to run in parallel. 7 | */ 8 | internal const val DEFAULT_ANALYZER_PARALLEL_COUNT = 2 9 | 10 | /** 11 | * An analyzer takes some data as an input, and returns an analyzed output. Analyzers should not 12 | * contain any state. They must define whether they can run on a multithreaded executor, and provide 13 | * a means of analyzing input data to return some form of result. 14 | */ 15 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 16 | interface Analyzer { 17 | suspend fun analyze(data: Input, state: State): Output 18 | } 19 | 20 | /** 21 | * A factory to create analyzers. 22 | */ 23 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 24 | interface AnalyzerFactory> { 25 | suspend fun newInstance(): AnalyzerType? 26 | } 27 | 28 | /** 29 | * A pool of analyzers. 30 | */ 31 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 32 | data class AnalyzerPool( 33 | val desiredAnalyzerCount: Int, 34 | val analyzers: List> 35 | ) { 36 | companion object { 37 | @Deprecated( 38 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 39 | replaceWith = ReplaceWith("StripeCardScan") 40 | ) 41 | suspend fun of( 42 | analyzerFactory: AnalyzerFactory>, 43 | desiredAnalyzerCount: Int = DEFAULT_ANALYZER_PARALLEL_COUNT, 44 | ) = AnalyzerPool( 45 | desiredAnalyzerCount = desiredAnalyzerCount, 46 | analyzers = (0 until desiredAnalyzerCount).mapNotNull { analyzerFactory.newInstance() } 47 | ) 48 | } 49 | 50 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 51 | fun closeAllAnalyzers() { 52 | // This should be using analyzers.forEach, but doing so seems to require API 24. It's unclear why this won't use 53 | // the kotlin.collections version of `forEach`, but it's not during compile. 54 | for (it in analyzers) { if (it is Closeable) it.close() } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/MachineState.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework 2 | 3 | import android.util.Log 4 | import com.getbouncer.scan.framework.time.Clock 5 | import com.getbouncer.scan.framework.time.ClockMark 6 | 7 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 8 | abstract class MachineState { 9 | 10 | /** 11 | * Keep track of when this state was reached 12 | */ 13 | protected open val reachedStateAt: ClockMark = Clock.markNow() 14 | 15 | override fun toString(): String = "${this::class.java.simpleName}(reachedStateAt=$reachedStateAt)" 16 | 17 | init { 18 | if (Config.isDebug) Log.d(Config.logTag, "${this::class.java.simpleName} machine state reached") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/Scan.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework 2 | 3 | import android.os.Build 4 | 5 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 6 | object Scan { 7 | 8 | /** 9 | * Determine if the device is running an ARM architecture. 10 | */ 11 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 12 | fun isDeviceArchitectureArm(): Boolean { 13 | val arch = System.getProperty("os.arch") ?: "" 14 | return "86" !in arch 15 | } 16 | 17 | /** 18 | * Determine the architecture of the device. 19 | */ 20 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 21 | fun getDeviceArchitecture(): String? { 22 | // From https://stackoverflow.com/questions/11989629/api-call-to-get-processor-architecture 23 | 24 | // Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64" 25 | // while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo). 26 | // Instead we search through the supported abi:s on the device, see: 27 | // http://developer.android.com/ndk/guides/abis.html 28 | 29 | // Note that we search for abi:s in preferred order (the ordering of the 30 | // Build.SUPPORTED_ABIS list) to avoid e.g. installing arm on an x86 system where arm 31 | // emulation is available. 32 | for (androidArch in Build.SUPPORTED_ABIS) { 33 | when (androidArch) { 34 | "arm64-v8a" -> return "aarch64" 35 | "armeabi-v7a" -> return "arm" 36 | "x86_64" -> return "x86_64" 37 | "x86" -> return "i686" 38 | } 39 | } 40 | return null 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/TrackedImage.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework 2 | 3 | /** 4 | * An image with a stat tracker. 5 | */ 6 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 7 | data class TrackedImage( 8 | val image: ImageType, 9 | val tracker: StatTracker, 10 | ) 11 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/api/NetworkResult.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.api 2 | 3 | @Deprecated( 4 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 5 | replaceWith = ReplaceWith("StripeCardScan"), 6 | ) 7 | sealed class NetworkResult(open val responseCode: Int) { 8 | @Deprecated( 9 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 10 | replaceWith = ReplaceWith("StripeCardScan"), 11 | ) 12 | data class Success(override val responseCode: Int, val body: Success) : NetworkResult(responseCode) 13 | @Deprecated( 14 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 15 | replaceWith = ReplaceWith("StripeCardScan"), 16 | ) 17 | data class Error(override val responseCode: Int, val error: Error) : NetworkResult(responseCode) 18 | @Deprecated( 19 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 20 | replaceWith = ReplaceWith("StripeCardScan"), 21 | ) 22 | data class Exception(override val responseCode: Int, val exception: Throwable) : NetworkResult(responseCode) 23 | } 24 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/AppInfo.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.api.dto 2 | 3 | import androidx.annotation.RestrictTo 4 | import com.getbouncer.scan.framework.util.AppDetails 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | 8 | @Serializable 9 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 10 | @Deprecated( 11 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 12 | replaceWith = ReplaceWith("StripeCardScan"), 13 | ) 14 | data class AppInfo( 15 | @SerialName("app_package_name") val appPackageName: String?, 16 | @SerialName("application_id") val applicationId: String, 17 | @SerialName("library_package_name") val libraryPackageName: String, 18 | @SerialName("sdk_version") val sdkVersion: String, 19 | @SerialName("sdk_version_code") val sdkVersionCode: Int, 20 | @SerialName("sdk_flavor") val sdkFlavor: String, 21 | @SerialName("is_debug_build") val isDebugBuild: Boolean 22 | ) { 23 | companion object { 24 | @Deprecated( 25 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 26 | replaceWith = ReplaceWith("StripeCardScan"), 27 | ) 28 | fun fromAppDetails(appDetails: AppDetails): AppInfo = AppInfo( 29 | appPackageName = appDetails.appPackageName, 30 | applicationId = appDetails.applicationId, 31 | libraryPackageName = appDetails.libraryPackageName, 32 | sdkVersion = appDetails.sdkVersion, 33 | sdkVersionCode = appDetails.sdkVersionCode, 34 | sdkFlavor = appDetails.sdkFlavor, 35 | isDebugBuild = appDetails.isDebugBuild 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/BouncerErrorResponse.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.api.dto 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | @Deprecated( 8 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 9 | replaceWith = ReplaceWith("StripeCardScan"), 10 | ) 11 | data class BouncerErrorResponse( 12 | @SerialName("status") val status: String, 13 | @SerialName("error_code") val errorCode: String, 14 | @SerialName("error_message") val errorMessage: String, 15 | @SerialName("error_payload") val errorPayload: String? 16 | ) 17 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/ClientDevice.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.api.dto 2 | 3 | import androidx.annotation.RestrictTo 4 | import com.getbouncer.scan.framework.util.Device 5 | import com.getbouncer.scan.framework.util.DeviceIds 6 | import kotlinx.serialization.SerialName 7 | import kotlinx.serialization.Serializable 8 | 9 | @Serializable 10 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 11 | @Deprecated( 12 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 13 | replaceWith = ReplaceWith("StripeCardScan"), 14 | ) 15 | data class ClientDevice( 16 | @SerialName("ids") val ids: ClientDeviceIds, 17 | @SerialName("type") val name: String, 18 | @SerialName("boot_count") val bootCount: Int, 19 | @SerialName("locale") val locale: String?, 20 | @SerialName("carrier") val carrier: String?, 21 | @SerialName("network_operator") val networkOperator: String?, 22 | @SerialName("phone_type") val phoneType: Int?, 23 | @SerialName("phone_count") val phoneCount: Int, 24 | @SerialName("os_version") val osVersion: String, 25 | @SerialName("platform") val platform: String 26 | ) { 27 | companion object { 28 | @Deprecated( 29 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 30 | replaceWith = ReplaceWith("StripeCardScan"), 31 | ) 32 | fun fromDevice(device: Device) = ClientDevice( 33 | ids = ClientDeviceIds.fromDeviceIds(device.ids), 34 | name = device.name, 35 | bootCount = device.bootCount, 36 | locale = device.locale, 37 | carrier = device.carrier, 38 | networkOperator = device.networkOperator, 39 | phoneType = device.phoneType, 40 | phoneCount = device.phoneCount, 41 | osVersion = device.osVersion.toString(), 42 | platform = device.platform 43 | ) 44 | } 45 | } 46 | 47 | @Serializable 48 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 49 | @Deprecated( 50 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 51 | replaceWith = ReplaceWith("StripeCardScan"), 52 | ) 53 | data class ClientDeviceIds( 54 | @SerialName("vendor_id") val androidId: String? 55 | ) { 56 | companion object { 57 | @Deprecated( 58 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 59 | replaceWith = ReplaceWith("StripeCardScan"), 60 | ) 61 | fun fromDeviceIds(deviceIds: DeviceIds) = ClientDeviceIds( 62 | androidId = deviceIds.androidId 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/ModelDetails.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.api.dto 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | @Deprecated( 8 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 9 | replaceWith = ReplaceWith("StripeCardScan"), 10 | ) 11 | data class ModelDetailsRequest( 12 | @SerialName("platform") val platform: String, 13 | @SerialName("model_class") val modelClass: String, 14 | @SerialName("model_framework_version") val modelFrameworkVersion: Int, 15 | @SerialName("cached_model_hash") val cachedModelHash: String?, 16 | @SerialName("cached_model_hash_algorithm") val cachedModelHashAlgorithm: String?, 17 | @SerialName("beta_opt_in") val betaOptIn: Boolean?, 18 | ) 19 | 20 | @Serializable 21 | @Deprecated( 22 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 23 | replaceWith = ReplaceWith("StripeCardScan"), 24 | ) 25 | data class ModelDetailsResponse( 26 | @SerialName("model_url") val url: String?, 27 | @SerialName("model_version") val modelVersion: String, 28 | @SerialName("model_hash") val hash: String, 29 | @SerialName("model_hash_algorithm") val hashAlgorithm: String, 30 | @SerialName("query_again_after_ms") val queryAgainAfterMs: Long? = 0, 31 | ) 32 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/ModelSignedUrlResponse.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.api.dto 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | @Deprecated( 8 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 9 | replaceWith = ReplaceWith("StripeCardScan"), 10 | ) 11 | data class ModelSignedUrlResponse( 12 | @SerialName("model_url") val modelUrl: String 13 | ) 14 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/api/dto/ValidateApiKeyResponse.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.api.dto 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | @Deprecated( 8 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 9 | replaceWith = ReplaceWith("StripeCardScan"), 10 | ) 11 | data class ValidateApiKeyResponse( 12 | @SerialName("is_valid_api_key") val isApiKeyValid: Boolean, 13 | @SerialName("invalid_key_reason") val keyInvalidReason: String? 14 | ) 15 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/exception/ImageTypeNotSupportedException.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.exception 2 | 3 | import java.lang.Exception 4 | 5 | @Deprecated( 6 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 7 | replaceWith = ReplaceWith("StripeCardScan"), 8 | ) 9 | class ImageTypeNotSupportedException(val imageType: Int) : Exception() 10 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/exception/InvalidBouncerApiKeyException.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.exception 2 | 3 | @Deprecated( 4 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 5 | replaceWith = ReplaceWith("StripeCardScan"), 6 | ) 7 | object InvalidBouncerApiKeyException : Exception() 8 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/image/ImageExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.image 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import android.graphics.ImageFormat 6 | import android.graphics.Rect 7 | import android.media.Image 8 | import android.renderscript.RenderScript 9 | import androidx.annotation.CheckResult 10 | import com.getbouncer.scan.framework.exception.ImageTypeNotSupportedException 11 | 12 | /** 13 | * Determine if this application supports an image format. 14 | */ 15 | @CheckResult 16 | @Deprecated( 17 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 18 | replaceWith = ReplaceWith("StripeCardScan"), 19 | ) 20 | fun Image.isSupportedFormat() = isSupportedFormat(this.format) 21 | 22 | /** 23 | * Determine if this application supports an image format. 24 | */ 25 | @CheckResult 26 | @Deprecated( 27 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 28 | replaceWith = ReplaceWith("StripeCardScan"), 29 | ) 30 | fun isSupportedFormat(imageFormat: Int) = when (imageFormat) { 31 | ImageFormat.YUV_420_888, ImageFormat.JPEG -> true 32 | ImageFormat.NV21 -> false // this fails on devices with android API 21. 33 | else -> false 34 | } 35 | 36 | /** 37 | * Convert an image to a bitmap for processing. This will throw an [ImageTypeNotSupportedException] 38 | * if the image type is not supported (see [isSupportedFormat]). 39 | */ 40 | @CheckResult 41 | @Throws(ImageTypeNotSupportedException::class) 42 | @Deprecated( 43 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 44 | replaceWith = ReplaceWith("StripeCardScan"), 45 | ) 46 | fun Image.toBitmap( 47 | renderScript: RenderScript, 48 | crop: Rect = Rect( 49 | 0, 50 | 0, 51 | this.width, 52 | this.height 53 | ), 54 | ): Bitmap = when (this.format) { 55 | ImageFormat.NV21 -> NV21Image(this).crop(crop).toBitmap(renderScript) 56 | ImageFormat.YUV_420_888 -> NV21Image(this).crop(crop).toBitmap(renderScript) 57 | ImageFormat.JPEG -> jpegToBitmap().crop(crop) 58 | else -> throw ImageTypeNotSupportedException(this.format) 59 | } 60 | 61 | @CheckResult 62 | private fun Image.jpegToBitmap(): Bitmap { 63 | require(format == ImageFormat.JPEG) { "Image is not in JPEG format" } 64 | 65 | val imageBuffer = planes[0].buffer 66 | val imageBytes = ByteArray(imageBuffer.remaining()) 67 | imageBuffer.get(imageBytes) 68 | return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) 69 | } 70 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/image/YuvImageExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.image 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import android.graphics.Rect 6 | import android.graphics.YuvImage 7 | import androidx.annotation.CheckResult 8 | import java.io.ByteArrayOutputStream 9 | 10 | /** 11 | * Convert a [YuvImage] to a [Bitmap]. This is not an efficient method since it uses an intermediate JPEG compression 12 | * and should be avoided if possible. 13 | */ 14 | @CheckResult 15 | @Deprecated("This method is inefficient and should be avoided if possible") 16 | fun YuvImage.toBitmap( 17 | crop: Rect = Rect( 18 | 0, 19 | 0, 20 | this.width, 21 | this.height 22 | ), 23 | quality: Int = 75 24 | ): Bitmap { 25 | val out = ByteArrayOutputStream() 26 | compressToJpeg(crop, quality, out) 27 | 28 | val imageBytes = out.toByteArray() 29 | return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) 30 | } 31 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/interop/BlockingAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.interop 2 | 3 | import com.getbouncer.scan.framework.Analyzer 4 | import com.getbouncer.scan.framework.AnalyzerFactory 5 | import com.getbouncer.scan.framework.AnalyzerPool 6 | import com.getbouncer.scan.framework.DEFAULT_ANALYZER_PARALLEL_COUNT 7 | import kotlinx.coroutines.runBlocking 8 | 9 | /** 10 | * An implementation of an analyzer that does not use suspending functions. This allows interoperability with java. 11 | */ 12 | @Deprecated( 13 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 14 | replaceWith = ReplaceWith("StripeCardScan"), 15 | ) 16 | abstract class BlockingAnalyzer : Analyzer { 17 | override suspend fun analyze(data: Input, state: State): Output = analyzeBlocking(data, state) 18 | 19 | abstract fun analyzeBlocking(data: Input, state: State): Output 20 | } 21 | 22 | /** 23 | * An implementation of an analyzer factory that does not use suspending functions. This allows interoperability with 24 | * java. 25 | */ 26 | @Deprecated( 27 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 28 | replaceWith = ReplaceWith("StripeCardScan"), 29 | ) 30 | abstract class BlockingAnalyzerFactory> : AnalyzerFactory { 31 | override suspend fun newInstance(): AnalyzerType? = newInstanceBlocking() 32 | 33 | abstract fun newInstanceBlocking(): AnalyzerType? 34 | } 35 | 36 | /** 37 | * An implementation of an analyzer pool factory that does not use suspending functions. This allows interoperability 38 | * with java. 39 | */ 40 | @Deprecated( 41 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 42 | replaceWith = ReplaceWith("StripeCardScan"), 43 | ) 44 | class BlockingAnalyzerPoolFactory @JvmOverloads constructor( 45 | private val analyzerFactory: AnalyzerFactory>, 46 | private val desiredAnalyzerCount: Int = DEFAULT_ANALYZER_PARALLEL_COUNT 47 | ) { 48 | fun buildAnalyzerPool() = AnalyzerPool( 49 | desiredAnalyzerCount = desiredAnalyzerCount, 50 | analyzers = (0 until desiredAnalyzerCount).mapNotNull { 51 | runBlocking { analyzerFactory.newInstance() } 52 | } 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/interop/JavaContinuation.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("Coroutine") 2 | package com.getbouncer.scan.framework.interop 3 | 4 | import android.util.Log 5 | import com.getbouncer.scan.framework.Config 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.runBlocking 8 | import kotlin.coroutines.Continuation 9 | import kotlin.coroutines.CoroutineContext 10 | import kotlin.coroutines.resume 11 | import kotlin.coroutines.resumeWithException 12 | 13 | /** 14 | * A utility class for calling suspend functions from java. This allows listening to a suspend function with callbacks. 15 | */ 16 | @Deprecated( 17 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 18 | replaceWith = ReplaceWith("StripeCardScan"), 19 | ) 20 | abstract class JavaContinuation @JvmOverloads constructor( 21 | runOn: CoroutineContext = Dispatchers.Default, 22 | private val listenOn: CoroutineContext = Dispatchers.Main 23 | ) : Continuation { 24 | override val context: CoroutineContext = runOn 25 | abstract fun onComplete(value: T) 26 | abstract fun onException(exception: Throwable) 27 | override fun resumeWith(result: Result) = result.fold( 28 | onSuccess = { 29 | runBlocking(listenOn) { 30 | onComplete(it) 31 | } 32 | }, 33 | onFailure = { 34 | runBlocking(listenOn) { 35 | onException(it) 36 | } 37 | } 38 | ) 39 | } 40 | 41 | /** 42 | * An empty continuation for ignoring results. 43 | */ 44 | @Deprecated( 45 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 46 | replaceWith = ReplaceWith("StripeCardScan"), 47 | ) 48 | class EmptyJavaContinuation : JavaContinuation() { 49 | override fun onComplete(value: T) { } 50 | override fun onException(exception: Throwable) { 51 | Log.e(Config.logTag, "Error in continuation", exception) 52 | } 53 | } 54 | 55 | /** 56 | * Resume a continuation with a value. 57 | */ 58 | @Deprecated( 59 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 60 | replaceWith = ReplaceWith("StripeCardScan"), 61 | ) 62 | fun Continuation.resumeJava(value: T) = this.resume(value) 63 | 64 | /** 65 | * Resume a continuation with an exception. 66 | */ 67 | @Deprecated( 68 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 69 | replaceWith = ReplaceWith("StripeCardScan"), 70 | ) 71 | fun Continuation.resumeWithExceptionJava(exception: Throwable) = this.resumeWithException(exception) 72 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/ml/ModelVersionTracker.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.ml 2 | 3 | private val MODEL_MAP = mutableMapOf>>() 4 | 5 | /** 6 | * Details about a model loaded into memory. 7 | */ 8 | @Deprecated( 9 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 10 | replaceWith = ReplaceWith("StripeCardScan"), 11 | ) 12 | data class ModelLoadDetails( 13 | val modelClass: String, 14 | val modelVersion: String, 15 | val modelFrameworkVersion: Int, 16 | val success: Boolean 17 | ) 18 | 19 | /** 20 | * When a ML model is loaded into memory, track the details of the model. 21 | */ 22 | @Deprecated( 23 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 24 | replaceWith = ReplaceWith("StripeCardScan"), 25 | ) 26 | fun trackModelLoaded(modelClass: String, modelVersion: String, modelFrameworkVersion: Int, success: Boolean) { 27 | MODEL_MAP.getOrPut(modelClass) { mutableSetOf() }.add(Triple(modelVersion, modelFrameworkVersion, success)) 28 | } 29 | 30 | /** 31 | * Get the full list of models that were loaded into memory during this session. 32 | */ 33 | @Deprecated( 34 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 35 | replaceWith = ReplaceWith("StripeCardScan"), 36 | ) 37 | fun getLoadedModelVersions(): List = MODEL_MAP.flatMap { entry -> 38 | entry.value.map { 39 | ModelLoadDetails( 40 | modelClass = entry.key, 41 | modelVersion = it.first, 42 | modelFrameworkVersion = it.second, 43 | success = it.third 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/ml/ssd/ClassifierScores.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.ml.ssd 2 | 3 | import com.getbouncer.scan.framework.util.updateEach 4 | import kotlin.math.exp 5 | 6 | typealias ClassifierScores = FloatArray 7 | 8 | /** 9 | * Compute softmax for the given row. This will replace each row value with a value normalized by 10 | * the sum of all the values in the row. 11 | */ 12 | @Deprecated( 13 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 14 | replaceWith = ReplaceWith("StripeCardScan"), 15 | ) 16 | fun ClassifierScores.softMax() { 17 | val rowSumExp = this.fold(0F) { acc, element -> acc + exp(element) } 18 | this.updateEach { exp(it) / rowSumExp } 19 | } 20 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/time/Coroutine.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.time 2 | 3 | import kotlin.math.roundToLong 4 | 5 | /** 6 | * Allow delaying for a specified duration 7 | */ 8 | @Deprecated( 9 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 10 | replaceWith = ReplaceWith("StripeCardScan"), 11 | ) 12 | suspend fun delay(duration: Duration) = 13 | kotlinx.coroutines.delay(duration.inMilliseconds.roundToLong()) 14 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/time/Rate.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.time 2 | 3 | /** 4 | * A rate of execution. 5 | */ 6 | @Deprecated( 7 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 8 | replaceWith = ReplaceWith("StripeCardScan"), 9 | ) 10 | data class Rate(val amount: Long, val duration: Duration) : Comparable { 11 | override fun compareTo(other: Rate): Int { 12 | return (other.duration / other.amount).compareTo(duration / amount) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/time/Timer.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.time 2 | 3 | import android.util.Log 4 | import com.getbouncer.scan.framework.Config 5 | import kotlinx.coroutines.runBlocking 6 | 7 | @Deprecated( 8 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 9 | replaceWith = ReplaceWith("StripeCardScan"), 10 | ) 11 | sealed class Timer { 12 | 13 | companion object { 14 | @Deprecated( 15 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 16 | replaceWith = ReplaceWith("StripeCardScan"), 17 | ) 18 | fun newInstance( 19 | tag: String, 20 | name: String, 21 | updateInterval: Duration = 2.seconds, 22 | enabled: Boolean = Config.isDebug 23 | ) = if (enabled) { 24 | LoggingTimer( 25 | tag, 26 | name, 27 | updateInterval 28 | ) 29 | } else { 30 | NoOpTimer 31 | } 32 | } 33 | 34 | /** 35 | * Log the duration of a single task and return the result from that task. 36 | * 37 | * TODO: use contracts when they are no longer experimental 38 | */ 39 | fun measure(taskName: String? = null, task: () -> T): T { 40 | // contract { callsInPlace(task, EXACTLY_ONCE) } 41 | return runBlocking { measureSuspend(taskName) { task() } } 42 | } 43 | 44 | abstract suspend fun measureSuspend(taskName: String? = null, task: suspend () -> T): T 45 | } 46 | 47 | private object NoOpTimer : Timer() { 48 | 49 | // TODO: use contracts when they are no longer experimental 50 | override suspend fun measureSuspend(taskName: String?, task: suspend () -> T): T { 51 | // contract { callsInPlace(task, EXACTLY_ONCE) } 52 | return task() 53 | } 54 | } 55 | 56 | private class LoggingTimer( 57 | private val tag: String, 58 | private val name: String, 59 | private val updateInterval: Duration 60 | ) : Timer() { 61 | private var executionCount = 0 62 | private var executionTotalDuration = Duration.ZERO 63 | private var updateClock = Clock.markNow() 64 | 65 | // TODO: use contracts when they are no longer experimental 66 | override suspend fun measureSuspend(taskName: String?, task: suspend () -> T): T { 67 | // contract { callsInPlace(task, EXACTLY_ONCE) } 68 | val (duration, result) = measureTime { task() } 69 | 70 | executionCount++ 71 | executionTotalDuration += duration 72 | 73 | if (updateClock.elapsedSince() > updateInterval) { 74 | updateClock = Clock.markNow() 75 | Log.d( 76 | tag, 77 | "$name${if (!taskName.isNullOrEmpty()) ".$taskName" else ""} executing on " + 78 | "thread ${Thread.currentThread().name} " + 79 | "AT ${executionCount / executionTotalDuration.inSeconds} FPS, " + 80 | "${executionTotalDuration.inMilliseconds / executionCount} MS/F" 81 | ) 82 | } 83 | return result 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/util/AppDetails.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.util 2 | 3 | import android.content.Context 4 | import com.getbouncer.scan.framework.BuildConfig 5 | 6 | @Deprecated( 7 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 8 | replaceWith = ReplaceWith("StripeCardScan"), 9 | ) 10 | data class AppDetails( 11 | val appPackageName: String?, 12 | val applicationId: String, 13 | val libraryPackageName: String, 14 | val sdkVersion: String, 15 | val sdkVersionCode: Int, 16 | val sdkFlavor: String, 17 | val isDebugBuild: Boolean 18 | ) { 19 | companion object { 20 | @JvmStatic 21 | fun fromContext(context: Context) = AppDetails( 22 | appPackageName = getAppPackageName(context), 23 | applicationId = getApplicationId(), 24 | libraryPackageName = getLibraryPackageName(), 25 | sdkVersion = getSdkVersion(), 26 | sdkVersionCode = getSdkVersionCode(), 27 | sdkFlavor = getSdkFlavor(), 28 | isDebugBuild = isDebugBuild() 29 | ) 30 | } 31 | } 32 | 33 | @Deprecated( 34 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 35 | replaceWith = ReplaceWith("StripeCardScan"), 36 | ) 37 | fun getAppPackageName(context: Context): String? = context.applicationContext.packageName 38 | 39 | private fun getApplicationId(): String = "" // no longer available in later versions of gradle. 40 | 41 | @Deprecated( 42 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 43 | replaceWith = ReplaceWith("StripeCardScan"), 44 | ) 45 | fun getLibraryPackageName(): String = BuildConfig.LIBRARY_PACKAGE_NAME 46 | 47 | @Deprecated( 48 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 49 | replaceWith = ReplaceWith("StripeCardScan"), 50 | ) 51 | fun getSdkVersion(): String = BuildConfig.SDK_VERSION_STRING 52 | 53 | private fun getSdkVersionCode(): Int = -1 // no longer available in later versions of gradle. 54 | 55 | @Deprecated( 56 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 57 | replaceWith = ReplaceWith("StripeCardScan"), 58 | ) 59 | fun getSdkFlavor(): String = BuildConfig.BUILD_TYPE 60 | 61 | private fun isDebugBuild(): Boolean = BuildConfig.DEBUG 62 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/util/File.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.util 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import java.io.File 6 | import java.io.FileInputStream 7 | import java.io.IOException 8 | import java.lang.Exception 9 | import java.security.MessageDigest 10 | import java.security.NoSuchAlgorithmException 11 | 12 | private val illegalFileNameCharacters = setOf('"', '*', '/', ':', '<', '>', '?', '\\', '|', '+', ',', ';', '=', '[', ']') 13 | 14 | /** 15 | * Sanitize the name of a file for storage 16 | */ 17 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 18 | fun sanitizeFileName(unsanitized: String) = 19 | unsanitized.map { char -> if (char in illegalFileNameCharacters) "_" else char }.joinToString("") 20 | 21 | /** 22 | * Determine if a [File] matches the expected [hash]. 23 | */ 24 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 25 | suspend fun fileMatchesHash(localFile: File, hash: String, hashAlgorithm: String) = try { 26 | hash == calculateHash(localFile, hashAlgorithm) 27 | } catch (t: Throwable) { 28 | false 29 | } 30 | 31 | /** 32 | * Calculate the hash of a file using the [hashAlgorithm]. 33 | */ 34 | @Throws(IOException::class, NoSuchAlgorithmException::class) 35 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 36 | suspend fun calculateHash(file: File, hashAlgorithm: String): String? = 37 | withContext(Dispatchers.IO) { 38 | if (file.exists()) { 39 | val digest = MessageDigest.getInstance(hashAlgorithm) 40 | FileInputStream(file).use { digest.update(it.readBytes()) } 41 | digest.digest().joinToString("") { "%02x".format(it) } 42 | } else { 43 | null 44 | } 45 | } 46 | 47 | /** 48 | * A file does not match the expected hash value. 49 | */ 50 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 51 | class HashMismatchException(val algorithm: String, val expected: String, val actual: String?) : 52 | Exception("Invalid hash result for algorithm '$algorithm'. Expected '$expected' but got '$actual'") { 53 | override fun toString() = "HashMismatchException(algorithm='$algorithm', expected='$expected', actual='$actual')" 54 | } 55 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/util/FrameSaver.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.util 2 | 3 | import androidx.annotation.CheckResult 4 | import kotlinx.coroutines.sync.Mutex 5 | import kotlinx.coroutines.sync.withLock 6 | import java.util.LinkedList 7 | 8 | /** 9 | * Save data frames for later retrieval. 10 | */ 11 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 12 | abstract class FrameSaver { 13 | 14 | private val saveFrameMutex = Mutex() 15 | private val savedFrames = mutableMapOf>() 16 | 17 | /** 18 | * Determine how frames should be classified using [getSaveFrameIdentifier], and then store them 19 | * in a map of frames based on that identifier. 20 | * 21 | * This method keeps track of the total number of saved frames. If the total number or total 22 | * size exceeds the maximum allowed, the oldest frames will be dropped. 23 | */ 24 | suspend fun saveFrame(frame: Frame, metaData: MetaData) { 25 | val identifier = getSaveFrameIdentifier(frame, metaData) ?: return 26 | return saveFrameMutex.withLock { 27 | val maxSavedFrames = getMaxSavedFrames(identifier) 28 | 29 | val frames = savedFrames.getOrPut(identifier) { LinkedList() } 30 | frames.addFirst(frame) 31 | 32 | while (frames.size > maxSavedFrames) { 33 | // saved frames is over size limit, reduce until it's not 34 | removeFrame(identifier, frames) 35 | } 36 | } 37 | } 38 | 39 | /** 40 | * Retrieve a copy of the list of saved frames. 41 | */ 42 | @CheckResult 43 | fun getSavedFrames(): Map> = savedFrames.toMap() 44 | 45 | /** 46 | * Clear all saved frames 47 | */ 48 | suspend fun reset() = saveFrameMutex.withLock { 49 | savedFrames.clear() 50 | } 51 | 52 | protected abstract fun getMaxSavedFrames(savedFrameIdentifier: Identifier): Int 53 | 54 | /** 55 | * Determine if a data frame should be saved for future processing. 56 | * 57 | * If this method returns a non-null string, the frame will be saved under that identifier. 58 | */ 59 | protected abstract fun getSaveFrameIdentifier(frame: Frame, metaData: MetaData): Identifier? 60 | 61 | /** 62 | * Remove a frame from this list. The most recently added frames will be at the beginning of 63 | * this list, while the least recently added frames will be at the end. 64 | */ 65 | protected open fun removeFrame(identifier: Identifier, frames: LinkedList) { 66 | frames.removeLast() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/util/ItemCounter.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.util 2 | 3 | import androidx.annotation.CheckResult 4 | import kotlinx.coroutines.runBlocking 5 | import kotlinx.coroutines.sync.Mutex 6 | import kotlinx.coroutines.sync.withLock 7 | import java.util.LinkedList 8 | 9 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 10 | interface ItemCounter { 11 | suspend fun countItem(item: T): Int 12 | 13 | fun getHighestCountItem(minCount: Int = 1): Pair? 14 | 15 | suspend fun reset() 16 | } 17 | 18 | /** 19 | * A class that counts and saves items. 20 | */ 21 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 22 | class ItemTotalCounter(firstValue: T? = null) : ItemCounter { 23 | private val storageMutex = Mutex() 24 | private val items = mutableMapOf() 25 | 26 | init { if (firstValue != null) runBlocking { countItem(firstValue) } } 27 | 28 | /** 29 | * Increment the count for the given item. Return the new count for the given item. 30 | */ 31 | override suspend fun countItem(item: T): Int = storageMutex.withLock { 32 | 1 + (items.put(item, 1 + (items[item] ?: 0)) ?: 0) 33 | } 34 | 35 | /** 36 | * Get the item that with the highest count. 37 | * 38 | * @param minCount the minimum times an item must have been counted. 39 | */ 40 | @CheckResult 41 | override fun getHighestCountItem(minCount: Int): Pair? = 42 | items 43 | .maxByOrNull { it.value } 44 | ?.let { if (items[it.key] ?: 0 >= minCount) it.value to it.key else null } 45 | 46 | /** 47 | * Reset all item counts. 48 | */ 49 | override suspend fun reset() = storageMutex.withLock { 50 | items.clear() 51 | } 52 | } 53 | 54 | /** 55 | * A class that keeps track of [maxItemsToTrack] recent items. 56 | */ 57 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 58 | class ItemRecencyCounter( 59 | private val maxItemsToTrack: Int, 60 | firstValue: T? = null 61 | ) : ItemCounter { 62 | private val storageMutex = Mutex() 63 | private val items = LinkedList() 64 | 65 | init { if (firstValue != null) runBlocking { countItem(firstValue) } } 66 | 67 | /** 68 | * Increment the count for the given item. Return the new count for the given item. 69 | */ 70 | override suspend fun countItem(item: T): Int = storageMutex.withLock { 71 | items.addFirst(item) 72 | 73 | while (items.size > maxItemsToTrack) { 74 | items.removeLast() 75 | } 76 | 77 | items.count { it == item } 78 | } 79 | 80 | /** 81 | * Get the item that with the highest count. 82 | * 83 | * @param minCount the minimum times an item must have been counted. 84 | */ 85 | @CheckResult 86 | override fun getHighestCountItem(minCount: Int): Pair? = 87 | items 88 | .groupingBy { it } 89 | .eachCount() 90 | .filter { it.value >= minCount } 91 | .maxByOrNull { it.value } 92 | ?.let { it.value to it.key } 93 | 94 | /** 95 | * Reset all item counts. 96 | */ 97 | override suspend fun reset() = storageMutex.withLock { 98 | items.clear() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /scan-framework/src/main/java/com/getbouncer/scan/framework/util/Retry.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.util 2 | 3 | import com.getbouncer.scan.framework.time.Duration 4 | import kotlinx.coroutines.delay 5 | 6 | private const val DEFAULT_RETRIES = 3 7 | 8 | /** 9 | * Call a given [task]. If the task throws an exception not included in the [excluding] list, retry 10 | * the task up to [times], each time after a delay of [retryDelay]. 11 | * 12 | * @param retryDelay the amount of time between a failed task and the next retry 13 | * @param times the number of times to retry the task 14 | * @param excluding a list of exceptions to fail immediately on 15 | * @param task the task to retry 16 | * 17 | * TODO: use contracts when they're no longer experimental 18 | */ 19 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 20 | suspend fun retry( 21 | retryDelay: Duration, 22 | times: Int = DEFAULT_RETRIES, 23 | excluding: List> = emptyList(), 24 | task: suspend () -> T 25 | ): T { 26 | // contract { callsInPlace(task, InvocationKind.AT_LEAST_ONCE) } 27 | var exception: Throwable? = null 28 | for (attempt in 1..times) { 29 | try { 30 | return task() 31 | } catch (t: Throwable) { 32 | exception = t 33 | if (t.javaClass in excluding) { 34 | throw t 35 | } 36 | if (attempt < times) { 37 | delay(retryDelay.inMilliseconds.toLong()) 38 | } 39 | } 40 | } 41 | 42 | if (exception != null) { 43 | throw exception 44 | } else { 45 | // This code should never be reached 46 | throw UnexpectedRetryException() 47 | } 48 | } 49 | 50 | /** 51 | * Call a given [task]. If the task throws an exception not included in the [excluding] list, retry 52 | * the task up to [times]. 53 | * 54 | * @param times the number of times to retry the task 55 | * @param excluding a list of exceptions to fail immediately on 56 | * @param task the task to retry 57 | * 58 | * TODO: use contracts when they're no longer experimental 59 | */ 60 | @Deprecated(message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan") 61 | fun retrySync( 62 | times: Int = DEFAULT_RETRIES, 63 | excluding: List> = emptyList(), 64 | task: () -> T 65 | ): T { 66 | // contract { callsInPlace(task, InvocationKind.AT_LEAST_ONCE) } 67 | var exception: Throwable? = null 68 | for (attempt in 1..times) { 69 | try { 70 | return task() 71 | } catch (t: Throwable) { 72 | exception = t 73 | if (t.javaClass in excluding) { 74 | throw t 75 | } 76 | } 77 | } 78 | 79 | if (exception != null) { 80 | throw exception 81 | } else { 82 | // This code should never be reached 83 | throw UnexpectedRetryException() 84 | } 85 | } 86 | 87 | /** 88 | * This exception should never be thrown, and therefore can be private. 89 | */ 90 | private class UnexpectedRetryException : Exception() 91 | -------------------------------------------------------------------------------- /scan-framework/src/test/java/com/getbouncer/scan/framework/AnalyzerTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework 2 | 3 | import androidx.test.filters.SmallTest 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.runBlockingTest 6 | import org.junit.Test 7 | import kotlin.test.assertEquals 8 | 9 | class AnalyzerTest { 10 | 11 | @Test 12 | @SmallTest 13 | @ExperimentalCoroutinesApi 14 | fun analyzerPoolCreateNormally() = runBlockingTest { 15 | class TestAnalyzerFactory : AnalyzerFactory { 16 | override suspend fun newInstance(): TestAnalyzer? = TestAnalyzer() 17 | } 18 | 19 | val analyzerPool = AnalyzerPool.of( 20 | analyzerFactory = TestAnalyzerFactory(), 21 | desiredAnalyzerCount = 12 22 | ) 23 | 24 | assertEquals(12, analyzerPool.desiredAnalyzerCount) 25 | assertEquals(12, analyzerPool.analyzers.size) 26 | assertEquals(3, analyzerPool.analyzers[0].analyze(1, 2)) 27 | } 28 | 29 | @Test 30 | @SmallTest 31 | @ExperimentalCoroutinesApi 32 | fun analyzerPoolCreateFailure() = runBlockingTest { 33 | class TestAnalyzerFactory : AnalyzerFactory { 34 | override suspend fun newInstance(): TestAnalyzer? = null 35 | } 36 | 37 | val analyzerPool = AnalyzerPool.of( 38 | analyzerFactory = TestAnalyzerFactory(), 39 | desiredAnalyzerCount = 12 40 | ) 41 | 42 | assertEquals(12, analyzerPool.desiredAnalyzerCount) 43 | assertEquals(0, analyzerPool.analyzers.size) 44 | } 45 | 46 | private class TestAnalyzer : Analyzer { 47 | override suspend fun analyze(data: Int, state: Int): Int = data + state 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /scan-framework/src/test/java/com/getbouncer/scan/framework/util/FrameSaverTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.util 2 | 3 | import kotlinx.coroutines.ExperimentalCoroutinesApi 4 | import kotlinx.coroutines.test.runBlockingTest 5 | import org.junit.Test 6 | import kotlin.test.assertEquals 7 | 8 | class FrameSaverTest { 9 | 10 | private class TestFrameSaver : FrameSaver() { 11 | override fun getMaxSavedFrames(savedFrameIdentifier: String): Int = 3 12 | 13 | override fun getSaveFrameIdentifier(frame: Int, metaData: Int): String? = when (metaData) { 14 | 1 -> "one" 15 | 2 -> "two" 16 | else -> "else" 17 | } 18 | } 19 | 20 | @Test 21 | @ExperimentalCoroutinesApi 22 | fun saveFrames() = runBlockingTest { 23 | val frameSaver = TestFrameSaver() 24 | 25 | frameSaver.saveFrame(1, 1) 26 | frameSaver.saveFrame(1, 1) 27 | frameSaver.saveFrame(1, 1) 28 | frameSaver.saveFrame(1, 1) 29 | frameSaver.saveFrame(2, 2) 30 | frameSaver.saveFrame(3, 3) 31 | frameSaver.saveFrame(4, 4) 32 | 33 | assertEquals( 34 | listOf(1, 1, 1), 35 | frameSaver.getSavedFrames()["one"]?.toList() 36 | ) 37 | 38 | assertEquals( 39 | listOf(2), 40 | frameSaver.getSavedFrames()["two"]?.toList() 41 | ) 42 | 43 | assertEquals( 44 | listOf(4, 3), 45 | frameSaver.getSavedFrames()["else"]?.toList() 46 | ) 47 | } 48 | 49 | @Test 50 | @ExperimentalCoroutinesApi 51 | fun doesNotLeakInternalMap() = runBlockingTest { 52 | val frameSaver = TestFrameSaver() 53 | 54 | frameSaver.saveFrame(1, 1) 55 | 56 | val savedFrames = frameSaver.getSavedFrames() 57 | 58 | frameSaver.reset() 59 | 60 | assertEquals(1, savedFrames["one"]?.first) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /scan-framework/src/test/java/com/getbouncer/scan/framework/util/RetryTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.framework.util 2 | 3 | import androidx.test.filters.SmallTest 4 | import com.getbouncer.scan.framework.time.milliseconds 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.test.runBlockingTest 7 | import org.junit.Test 8 | import kotlin.test.assertEquals 9 | import kotlin.test.assertFailsWith 10 | 11 | class RetryTest { 12 | 13 | @Test 14 | @SmallTest 15 | @ExperimentalCoroutinesApi 16 | fun retry_succeedsFirst() = runBlockingTest { 17 | var executions = 0 18 | 19 | assertEquals( 20 | 1, 21 | retry(1.milliseconds) { 22 | executions++ 23 | 1 24 | } 25 | ) 26 | assertEquals(1, executions) 27 | } 28 | 29 | @Test 30 | @SmallTest 31 | @ExperimentalCoroutinesApi 32 | fun retry_succeedsSecond() = runBlockingTest { 33 | var executions = 0 34 | 35 | assertEquals( 36 | 1, 37 | retry(1.milliseconds) { 38 | executions++ 39 | if (executions == 2) { 40 | 1 41 | } else { 42 | throw RuntimeException() 43 | } 44 | } 45 | ) 46 | assertEquals(2, executions) 47 | } 48 | 49 | @Test 50 | @SmallTest 51 | @ExperimentalCoroutinesApi 52 | fun retry_fails() = runBlockingTest { 53 | var executions = 0 54 | 55 | assertFailsWith { 56 | retry(1.milliseconds) { 57 | executions++ 58 | throw RuntimeException() 59 | } 60 | } 61 | assertEquals(3, executions) 62 | } 63 | 64 | @Test 65 | @SmallTest 66 | @ExperimentalCoroutinesApi 67 | fun retry_excluding() = runBlockingTest { 68 | var executions = 0 69 | 70 | assertFailsWith { 71 | retry(1.milliseconds, excluding = listOf(RuntimeException::class.java)) { 72 | executions++ 73 | throw RuntimeException() 74 | } 75 | } 76 | assertEquals(1, executions) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /scan-payment-full/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /scan-payment-full/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlinx-serialization' 4 | 5 | android { 6 | compileSdkVersion 30 7 | buildToolsVersion '30.0.3' 8 | 9 | defaultConfig { 10 | minSdkVersion 21 11 | targetSdkVersion 30 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles "consumer-rules.pro" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 21 | } 22 | } 23 | 24 | testOptions { 25 | unitTests.includeAndroidResources = true 26 | } 27 | 28 | lintOptions { 29 | enable "Interoperability" 30 | } 31 | 32 | packagingOptions { 33 | pickFirst 'META-INF/AL2.0' 34 | pickFirst 'META-INF/LGPL2.1' 35 | } 36 | 37 | aaptOptions { 38 | noCompress "tflite" 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation fileTree(dir: "libs", include: ["*.jar"]) 44 | api project(":scan-framework") 45 | api project(":scan-payment") 46 | 47 | implementation "androidx.core:core-ktx:[1.3.1,1.6.0]" 48 | } 49 | 50 | dependencies { 51 | testImplementation "androidx.test:core:1.4.0" 52 | testImplementation "androidx.test:runner:1.4.0" 53 | testImplementation "junit:junit:4.13.2" 54 | testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 55 | } 56 | 57 | dependencies { 58 | androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" 59 | androidTestImplementation "androidx.test.ext:junit:1.1.3" 60 | androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 61 | androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1" 62 | } 63 | 64 | apply from: "deploy.gradle" 65 | -------------------------------------------------------------------------------- /scan-payment-full/src/androidTest/res/drawable/card_no_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment-full/src/androidTest/res/drawable/card_no_card.png -------------------------------------------------------------------------------- /scan-payment-full/src/androidTest/res/drawable/card_no_pan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment-full/src/androidTest/res/drawable/card_no_pan.png -------------------------------------------------------------------------------- /scan-payment-full/src/androidTest/res/drawable/card_pan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment-full/src/androidTest/res/drawable/card_pan.png -------------------------------------------------------------------------------- /scan-payment-full/src/androidTest/res/drawable/ocr_card_numbers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment-full/src/androidTest/res/drawable/ocr_card_numbers.png -------------------------------------------------------------------------------- /scan-payment-full/src/androidTest/res/drawable/ocr_card_numbers_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment-full/src/androidTest/res/drawable/ocr_card_numbers_qr.png -------------------------------------------------------------------------------- /scan-payment-full/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /scan-payment-full/src/main/assets/darknite_1_1_1_16.tflite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment-full/src/main/assets/darknite_1_1_1_16.tflite -------------------------------------------------------------------------------- /scan-payment-full/src/main/assets/ux_0_5_23_16.tflite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment-full/src/main/assets/ux_0_5_23_16.tflite -------------------------------------------------------------------------------- /scan-payment-minimal/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /scan-payment-minimal/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlinx-serialization' 4 | 5 | android { 6 | compileSdkVersion 30 7 | buildToolsVersion '30.0.3' 8 | 9 | defaultConfig { 10 | minSdkVersion 21 11 | targetSdkVersion 30 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles "consumer-rules.pro" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 21 | } 22 | } 23 | 24 | testOptions { 25 | unitTests.includeAndroidResources = true 26 | } 27 | 28 | lintOptions { 29 | enable "Interoperability" 30 | } 31 | 32 | packagingOptions { 33 | pickFirst 'META-INF/AL2.0' 34 | pickFirst 'META-INF/LGPL2.1' 35 | } 36 | } 37 | 38 | dependencies { 39 | implementation fileTree(dir: "libs", include: ["*.jar"]) 40 | api project(":scan-framework") 41 | api project(":scan-payment") 42 | 43 | implementation "androidx.core:core-ktx:[1.3.1,1.6.0]" 44 | } 45 | 46 | dependencies { 47 | testImplementation "androidx.test:core:1.4.0" 48 | testImplementation "androidx.test:runner:1.4.0" 49 | testImplementation "junit:junit:4.13.2" 50 | testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 51 | } 52 | 53 | dependencies { 54 | androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" 55 | androidTestImplementation "androidx.test.ext:junit:1.1.3" 56 | androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 57 | androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1" 58 | } 59 | 60 | apply from: "deploy.gradle" 61 | -------------------------------------------------------------------------------- /scan-payment-minimal/src/androidTest/res/drawable/card_no_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment-minimal/src/androidTest/res/drawable/card_no_card.png -------------------------------------------------------------------------------- /scan-payment-minimal/src/androidTest/res/drawable/card_no_pan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment-minimal/src/androidTest/res/drawable/card_no_pan.png -------------------------------------------------------------------------------- /scan-payment-minimal/src/androidTest/res/drawable/card_pan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment-minimal/src/androidTest/res/drawable/card_pan.png -------------------------------------------------------------------------------- /scan-payment-minimal/src/androidTest/res/drawable/ocr_card_numbers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment-minimal/src/androidTest/res/drawable/ocr_card_numbers.png -------------------------------------------------------------------------------- /scan-payment-minimal/src/androidTest/res/drawable/ocr_card_numbers_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment-minimal/src/androidTest/res/drawable/ocr_card_numbers_qr.png -------------------------------------------------------------------------------- /scan-payment-minimal/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /scan-payment-minimal/src/main/assets/UX.0.25.106.8.tflite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment-minimal/src/main/assets/UX.0.25.106.8.tflite -------------------------------------------------------------------------------- /scan-payment-minimal/src/main/assets/mb2_brex_metal_synthetic_svhnextra_epoch_3_5_98_8.tflite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment-minimal/src/main/assets/mb2_brex_metal_synthetic_svhnextra_epoch_3_5_98_8.tflite -------------------------------------------------------------------------------- /scan-payment/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /scan-payment/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlinx-serialization' 4 | apply plugin: 'kotlin-parcelize' 5 | 6 | android { 7 | compileSdkVersion 30 8 | buildToolsVersion '30.0.3' 9 | 10 | defaultConfig { 11 | minSdkVersion 21 12 | targetSdkVersion 30 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 22 | } 23 | } 24 | 25 | testOptions { 26 | unitTests.includeAndroidResources = true 27 | } 28 | 29 | lintOptions { 30 | enable "Interoperability" 31 | } 32 | 33 | packagingOptions { 34 | pickFirst 'META-INF/AL2.0' 35 | pickFirst 'META-INF/LGPL2.1' 36 | } 37 | 38 | compileOptions { 39 | sourceCompatibility JavaVersion.VERSION_1_8 40 | targetCompatibility JavaVersion.VERSION_1_8 41 | } 42 | 43 | kotlinOptions { 44 | jvmTarget = JavaVersion.VERSION_1_8.toString() 45 | } 46 | } 47 | 48 | dependencies { 49 | implementation fileTree(dir: "libs", include: ["*.jar"]) 50 | implementation project(":scan-framework") 51 | 52 | implementation "androidx.core:core-ktx:[1.3.1,1.6.0]" 53 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:[1.4.0,1.5.1]" 54 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:[1.4.0,1.5.1]" 55 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:[1.1.0,1.2.2]" 56 | 57 | // Allow the user to specify their own version of Tensorflow Lite to include 58 | runtimeOnly project(":tensorflow-lite") 59 | compileOnly "org.tensorflow:tensorflow-lite:2.4.0" 60 | } 61 | 62 | dependencies { 63 | testImplementation "androidx.test:core:1.4.0" 64 | testImplementation "androidx.test:runner:1.4.0" 65 | testImplementation "junit:junit:4.13.2" 66 | testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 67 | } 68 | 69 | dependencies { 70 | androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" 71 | androidTestImplementation "androidx.test.ext:junit:1.1.3" 72 | androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 73 | androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1" 74 | } 75 | 76 | apply from: "deploy.gradle" 77 | -------------------------------------------------------------------------------- /scan-payment/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment/consumer-rules.pro -------------------------------------------------------------------------------- /scan-payment/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keep class org.tensorflow.lite.Interpreter { *; } 24 | -------------------------------------------------------------------------------- /scan-payment/src/androidTest/java/com/getbouncer/scan/payment/PaymentCardAndroidTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment 2 | 3 | import androidx.test.filters.SmallTest 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import com.getbouncer.scan.payment.card.CardType 6 | import com.getbouncer.scan.payment.card.getCardType 7 | import kotlinx.coroutines.ExperimentalCoroutinesApi 8 | import kotlinx.coroutines.runBlocking 9 | import org.junit.Test 10 | import kotlin.test.assertEquals 11 | 12 | class PaymentCardAndroidTest { 13 | 14 | private val appContext = InstrumentationRegistry.getInstrumentation().targetContext 15 | 16 | @Test 17 | @SmallTest 18 | @ExperimentalCoroutinesApi 19 | fun paymentCardTypeDebit() = runBlocking { 20 | assertEquals(CardType.Debit, getCardType(appContext, "349011")) 21 | assertEquals(CardType.Credit, getCardType(appContext, "648298")) 22 | assertEquals(CardType.Credit, getCardType(appContext, "648299")) 23 | assertEquals(CardType.Prepaid, getCardType(appContext, "531306")) 24 | assertEquals(CardType.Unknown, getCardType(appContext, "123456")) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scan-payment/src/androidTest/java/com/getbouncer/scan/payment/ml/ExpiryDetectTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment.ml 2 | 3 | import androidx.test.filters.SmallTest 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import com.getbouncer.scan.framework.Config 6 | import kotlinx.coroutines.runBlocking 7 | import org.junit.After 8 | import org.junit.Before 9 | import org.junit.Test 10 | import kotlin.test.assertNotNull 11 | 12 | class ExpiryDetectTest { 13 | private val testContext = InstrumentationRegistry.getInstrumentation().context 14 | 15 | @Before 16 | fun before() { 17 | Config.apiKey = "qOJ_fF-WLDMbG05iBq5wvwiTNTmM2qIn" 18 | } 19 | 20 | @After 21 | fun after() { 22 | Config.apiKey = null 23 | } 24 | 25 | /** 26 | * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with 27 | * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or 28 | * withContext(Dispatchers.Default). 29 | * 30 | * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details. 31 | */ 32 | @Test 33 | @SmallTest 34 | fun createsInterpreter() = runBlocking { 35 | val fetcher = ExpiryDetect.ModelFetcher(testContext) 36 | fetcher.clearCache() 37 | 38 | val factory = ExpiryDetect.Factory(testContext, fetcher.fetchData(forImmediateUse = false, isOptional = false)) 39 | 40 | assertNotNull(factory.newInstance()) 41 | }.let { } 42 | 43 | @Test 44 | @SmallTest 45 | fun createsValidOutput() { 46 | // TODO: add resources and test the object detector 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /scan-payment/src/androidTest/java/com/getbouncer/scan/payment/ml/TextDetectTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment.ml 2 | 3 | import androidx.test.filters.SmallTest 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import com.getbouncer.scan.framework.Config 6 | import kotlinx.coroutines.runBlocking 7 | import org.junit.After 8 | import org.junit.Before 9 | import org.junit.Test 10 | import kotlin.test.assertNotNull 11 | 12 | class TextDetectTest { 13 | private val testContext = InstrumentationRegistry.getInstrumentation().context 14 | 15 | @Before 16 | fun before() { 17 | Config.apiKey = "qOJ_fF-WLDMbG05iBq5wvwiTNTmM2qIn" 18 | } 19 | 20 | @After 21 | fun after() { 22 | Config.apiKey = null 23 | } 24 | 25 | /** 26 | * TODO: this method should use runBlockingTest instead of runBlocking. However, an issue with 27 | * runBlockingTest currently fails when functions under test use withContext(Dispatchers.IO) or 28 | * withContext(Dispatchers.Default). 29 | * 30 | * See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 for details. 31 | */ 32 | @Test 33 | @SmallTest 34 | fun createsInterpreter() = runBlocking { 35 | val fetcher = TextDetect.ModelFetcher(testContext) 36 | fetcher.clearCache() 37 | 38 | val factory = TextDetect.Factory(testContext, fetcher.fetchData(forImmediateUse = false, isOptional = false)) 39 | 40 | assertNotNull(factory.newInstance()) 41 | }.let { } 42 | 43 | @Test 44 | @SmallTest 45 | fun createsValidOutput() { 46 | // TODO: add resources and test the object detector 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /scan-payment/src/androidTest/res/drawable/card_no_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment/src/androidTest/res/drawable/card_no_card.png -------------------------------------------------------------------------------- /scan-payment/src/androidTest/res/drawable/card_no_pan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment/src/androidTest/res/drawable/card_no_pan.png -------------------------------------------------------------------------------- /scan-payment/src/androidTest/res/drawable/card_pan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment/src/androidTest/res/drawable/card_pan.png -------------------------------------------------------------------------------- /scan-payment/src/androidTest/res/drawable/ocr_card_numbers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment/src/androidTest/res/drawable/ocr_card_numbers.png -------------------------------------------------------------------------------- /scan-payment/src/androidTest/res/drawable/ocr_card_numbers_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment/src/androidTest/res/drawable/ocr_card_numbers_qr.png -------------------------------------------------------------------------------- /scan-payment/src/androidTest/res/raw/sample_bitmap.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment/src/androidTest/res/raw/sample_bitmap.bin -------------------------------------------------------------------------------- /scan-payment/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/FrameDetails.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | @Deprecated( 7 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 8 | replaceWith = ReplaceWith("StripeCardScan"), 9 | ) 10 | data class FrameDetails( 11 | val panSideConfidence: Float, 12 | val noPanSideConfidence: Float, 13 | val noCardConfidence: Float, 14 | val hasPan: Boolean, 15 | ) 16 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/ModelManager.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment 2 | 3 | import android.content.Context 4 | import androidx.annotation.VisibleForTesting 5 | import com.getbouncer.scan.framework.FetchedData 6 | import com.getbouncer.scan.framework.Fetcher 7 | import kotlinx.coroutines.sync.Mutex 8 | import kotlinx.coroutines.sync.withLock 9 | 10 | @Deprecated( 11 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 12 | replaceWith = ReplaceWith("StripeCardScan"), 13 | ) 14 | abstract class ModelManager { 15 | private lateinit var fetcher: Fetcher 16 | private val fetcherMutex = Mutex() 17 | 18 | private var onFetch: ((success: Boolean) -> Unit)? = null 19 | 20 | suspend fun fetchModel(context: Context, forImmediateUse: Boolean, isOptional: Boolean = false): FetchedData { 21 | fetcherMutex.withLock { 22 | if (!this::fetcher.isInitialized) { 23 | fetcher = getModelFetcher(context.applicationContext) 24 | } 25 | } 26 | return fetcher.fetchData(forImmediateUse, isOptional).also { 27 | onFetch?.invoke(it.successfullyFetched) 28 | } 29 | } 30 | 31 | suspend fun isReady() = fetcherMutex.withLock { 32 | if (this::fetcher.isInitialized) fetcher.isCached() else false 33 | } 34 | 35 | @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 36 | abstract fun getModelFetcher(context: Context): Fetcher 37 | } 38 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/TextDetectModelManager.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment 2 | 3 | import android.content.Context 4 | import com.getbouncer.scan.framework.Fetcher 5 | import com.getbouncer.scan.payment.ml.TextDetect 6 | 7 | @Deprecated( 8 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 9 | replaceWith = ReplaceWith("StripeCardScan"), 10 | ) 11 | object TextDetectModelManager : ModelManager() { 12 | override fun getModelFetcher(context: Context): Fetcher = TextDetect.ModelFetcher(context) 13 | } 14 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/card/CardIssuer.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment.card 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | /** 7 | * A list of supported card issuers. 8 | */ 9 | @Deprecated( 10 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 11 | replaceWith = ReplaceWith("StripeCardScan"), 12 | ) 13 | sealed class CardIssuer(open val displayName: String) : Parcelable { 14 | @Parcelize 15 | @Deprecated( 16 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 17 | replaceWith = ReplaceWith("StripeCardScan"), 18 | ) 19 | object AmericanExpress : CardIssuer("American Express") 20 | 21 | @Parcelize 22 | @Deprecated( 23 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 24 | replaceWith = ReplaceWith("StripeCardScan"), 25 | ) 26 | data class Custom(override val displayName: String) : CardIssuer(displayName) 27 | 28 | @Parcelize 29 | @Deprecated( 30 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 31 | replaceWith = ReplaceWith("StripeCardScan"), 32 | ) 33 | object DinersClub : CardIssuer("Diners Club") 34 | 35 | @Parcelize 36 | @Deprecated( 37 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 38 | replaceWith = ReplaceWith("StripeCardScan"), 39 | ) 40 | object Discover : CardIssuer("Discover") 41 | 42 | @Parcelize 43 | @Deprecated( 44 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 45 | replaceWith = ReplaceWith("StripeCardScan"), 46 | ) 47 | object JCB : CardIssuer("JCB") 48 | 49 | @Parcelize 50 | @Deprecated( 51 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 52 | replaceWith = ReplaceWith("StripeCardScan"), 53 | ) 54 | object MasterCard : CardIssuer("MasterCard") 55 | 56 | @Parcelize 57 | @Deprecated( 58 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 59 | replaceWith = ReplaceWith("StripeCardScan"), 60 | ) 61 | object UnionPay : CardIssuer("UnionPay") 62 | 63 | @Parcelize 64 | @Deprecated( 65 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 66 | replaceWith = ReplaceWith("StripeCardScan"), 67 | ) 68 | object Unknown : CardIssuer("Unknown") 69 | 70 | @Parcelize 71 | @Deprecated( 72 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 73 | replaceWith = ReplaceWith("StripeCardScan"), 74 | ) 75 | object Visa : CardIssuer("Visa") 76 | } 77 | 78 | /** 79 | * Format the card network as a human readable format. 80 | */ 81 | @Deprecated( 82 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 83 | replaceWith = ReplaceWith("StripeCardScan"), 84 | ) 85 | fun formatIssuer(issuer: CardIssuer): String = issuer.displayName 86 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/card/CardType.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment.card 2 | 3 | import android.content.Context 4 | import androidx.annotation.CheckResult 5 | import androidx.annotation.RawRes 6 | import com.getbouncer.scan.framework.util.cacheFirstResultSuspend 7 | import com.getbouncer.scan.payment.R 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.withContext 10 | import java.io.ByteArrayInputStream 11 | import java.io.FileInputStream 12 | import java.nio.ByteBuffer 13 | import java.util.zip.ZipInputStream 14 | 15 | @Deprecated( 16 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 17 | replaceWith = ReplaceWith("StripeCardScan"), 18 | ) 19 | sealed class CardType { 20 | object Credit : CardType() 21 | object Debit : CardType() 22 | object Prepaid : CardType() 23 | object Unknown : CardType() 24 | } 25 | 26 | @Deprecated( 27 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 28 | replaceWith = ReplaceWith("StripeCardScan"), 29 | ) 30 | val getTypeTable: suspend (Context) -> Map = cacheFirstResultSuspend { context: Context -> 31 | readRawZippedResourceToStringArray(context, R.raw.payment_card_types).map { 32 | val fields = it.split(",") 33 | fields[0].toInt()..fields[1].toInt() to when (fields[2]) { 34 | "CREDIT" -> CardType.Credit 35 | "DEBIT" -> CardType.Debit 36 | "PREPAID" -> CardType.Prepaid 37 | else -> CardType.Unknown 38 | } 39 | }.toMap() 40 | } 41 | 42 | /** 43 | * Read a raw resource into a [ByteBuffer]. 44 | */ 45 | @CheckResult 46 | private suspend fun readRawZippedResourceToStringArray(context: Context, @RawRes resourceId: Int) = 47 | withContext(Dispatchers.IO) { 48 | val byteArray = context.resources.openRawResourceFd(resourceId).use { fileDescriptor -> 49 | val byteBuffer = ByteBuffer.allocate(fileDescriptor.declaredLength.toInt()) 50 | FileInputStream(fileDescriptor.fileDescriptor).use { fileInputStream -> 51 | fileInputStream.channel.read(byteBuffer, fileDescriptor.startOffset) 52 | byteBuffer.rewind() 53 | byteBuffer.array() 54 | } 55 | } 56 | 57 | ByteArrayInputStream(byteArray).use { byteArrayInputStream -> 58 | ZipInputStream(byteArrayInputStream).use { zipInputStream -> 59 | var entry = zipInputStream.nextEntry 60 | while (entry != null && !entry.name.endsWith(".txt")) { 61 | entry = zipInputStream.nextEntry 62 | } 63 | if (entry != null) { 64 | zipInputStream.bufferedReader().readLines() 65 | } else { 66 | emptyList() 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/card/PanValidator.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment.card 2 | 3 | /** 4 | * A class that provides a method to determine if a PAN is valid. 5 | */ 6 | @Deprecated( 7 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 8 | replaceWith = ReplaceWith("StripeCardScan"), 9 | ) 10 | interface PanValidator { 11 | @Deprecated( 12 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 13 | replaceWith = ReplaceWith("StripeCardScan"), 14 | ) 15 | fun isValidPan(pan: String): Boolean 16 | 17 | @Deprecated( 18 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 19 | replaceWith = ReplaceWith("StripeCardScan"), 20 | ) 21 | operator fun plus(other: PanValidator): PanValidator = CompositePanValidator(this, other) 22 | } 23 | 24 | /** 25 | * A [PanValidator] comprised of two separate validators. 26 | */ 27 | private class CompositePanValidator( 28 | private val validator1: PanValidator, 29 | private val validator2: PanValidator 30 | ) : PanValidator { 31 | override fun isValidPan(pan: String): Boolean = 32 | validator1.isValidPan(pan) && validator2.isValidPan(pan) 33 | } 34 | 35 | /** 36 | * A [PanValidator] that ensures the PAN is of a valid length. 37 | */ 38 | internal object LengthPanValidator : PanValidator { 39 | override fun isValidPan(pan: String): Boolean { 40 | val iinData = getIssuerData(pan) ?: return false 41 | return pan.length in iinData.panLengths 42 | } 43 | } 44 | 45 | /** 46 | * A [PanValidator] that performs the Luhn check for validation. 47 | * 48 | * see https://en.wikipedia.org/wiki/Luhn_algorithm 49 | */ 50 | internal object LuhnPanValidator : PanValidator { 51 | override fun isValidPan(pan: String): Boolean { 52 | if (pan.isEmpty()) { 53 | return false 54 | } 55 | 56 | fun doubleDigit(digit: Int) = if (digit * 2 > 9) digit * 2 - 9 else digit * 2 57 | 58 | var sum = pan.takeLast(1).toInt() 59 | val parity = pan.length % 2 60 | 61 | for (i in 0 until pan.length - 1) { 62 | sum += if (i % 2 == parity) { 63 | doubleDigit(Character.getNumericValue(pan[i])) 64 | } else { 65 | Character.getNumericValue(pan[i]) 66 | } 67 | } 68 | 69 | return sum % 10 == 0 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/card/PaymentCard.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment.card 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | @Deprecated( 8 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 9 | replaceWith = ReplaceWith("StripeCardScan"), 10 | ) 11 | data class PaymentCard( 12 | val pan: String?, 13 | val expiry: PaymentCardExpiry?, 14 | val issuer: CardIssuer?, 15 | val cvc: String?, 16 | val legalName: String? 17 | ) : Parcelable 18 | 19 | @Parcelize 20 | @Deprecated( 21 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 22 | replaceWith = ReplaceWith("StripeCardScan"), 23 | ) 24 | data class PaymentCardExpiry(val day: String?, val month: String, val year: String) : Parcelable 25 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/card/RequiresMatchingCard.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment.card 2 | 3 | /** 4 | * An interface for a class that requires a card to match. This provides the methods used to determine if a given card 5 | * matches or does not match the required card. 6 | */ 7 | @Deprecated( 8 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 9 | replaceWith = ReplaceWith("StripeCardScan"), 10 | ) 11 | interface RequiresMatchingCard { 12 | val requiredIin: String? 13 | val requiredLastFour: String? 14 | 15 | /** 16 | * Returns true if the card matches the [requiredIin] and/or [requiredLastFour], or if the two fields are null. 17 | * 18 | * TODO: use contracts when they are no longer experimental 19 | */ 20 | @Deprecated( 21 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 22 | replaceWith = ReplaceWith("StripeCardScan"), 23 | ) 24 | fun matchesRequiredCard(pan: String?): Boolean { 25 | // contract { returns(true) implies (pan != null) } 26 | return pan != null && isValidPan(pan) && panMatches(requiredIin, requiredLastFour, pan) 27 | } 28 | 29 | /** 30 | * Returns true if the required card fields are non-null and the given pan does not match. 31 | * 32 | * TODO: use contracts when they are no longer experimental 33 | */ 34 | @Deprecated( 35 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 36 | replaceWith = ReplaceWith("StripeCardScan"), 37 | ) 38 | fun doesNotMatchRequiredCard(pan: String?): Boolean { 39 | // contract { returns(true) implies (pan != null) } 40 | return pan != null && isValidPan(pan) && !panMatches(requiredIin, requiredLastFour, pan) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/ml/AlphabetDetectModelManager.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment.ml 2 | 3 | import android.content.Context 4 | import com.getbouncer.scan.framework.Fetcher 5 | import com.getbouncer.scan.payment.ModelManager 6 | 7 | @Deprecated( 8 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 9 | replaceWith = ReplaceWith("StripeCardScan"), 10 | ) 11 | object AlphabetDetectModelManager : ModelManager() { 12 | override fun getModelFetcher(context: Context): Fetcher = AlphabetDetect.ModelFetcher(context) 13 | } 14 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/ml/CardDetectModelManager.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment.ml 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import com.getbouncer.scan.framework.Config 6 | import com.getbouncer.scan.framework.Fetcher 7 | import com.getbouncer.scan.framework.UpdatingModelWebFetcher 8 | import com.getbouncer.scan.framework.UpdatingResourceFetcher 9 | import com.getbouncer.scan.framework.assetFileExists 10 | import com.getbouncer.scan.payment.ModelManager 11 | 12 | private const val CARD_DETECT_ASSET_FULL = "ux_0_5_23_16.tflite" 13 | private const val CARD_DETECT_ASSET_MINIMAL = "UX.0.25.106.8.tflite" 14 | 15 | @Deprecated( 16 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 17 | replaceWith = ReplaceWith("StripeCardScan"), 18 | ) 19 | object CardDetectModelManager : ModelManager() { 20 | override fun getModelFetcher(context: Context): Fetcher = when { 21 | assetFileExists(context, CARD_DETECT_ASSET_FULL) -> { 22 | Log.d(Config.logTag, "Full card detect available in assets") 23 | object : UpdatingResourceFetcher(context) { 24 | override val assetFileName: String = CARD_DETECT_ASSET_FULL 25 | override val resourceModelVersion: String = "0.5.23.16" 26 | override val resourceModelHash: String = "ea51ca5c693a4b8733b1cf1a63557a713a13fabf0bcb724385077694e63a51a7" 27 | override val resourceModelHashAlgorithm: String = "SHA-256" 28 | override val modelClass: String = "card_detection" 29 | override val modelFrameworkVersion: Int = 1 30 | } 31 | } 32 | assetFileExists(context, CARD_DETECT_ASSET_MINIMAL) -> { 33 | Log.d(Config.logTag, "Minimal card detect available in assets") 34 | object : UpdatingResourceFetcher(context) { 35 | override val assetFileName: String = CARD_DETECT_ASSET_MINIMAL 36 | override val resourceModelVersion: String = "0.25.106.8" 37 | override val resourceModelHash: String = "c2a39c9034a9f0073933488021676c46910cec0d1bf330ac22a908dcd7dd448a" 38 | override val resourceModelHashAlgorithm: String = "SHA-256" 39 | override val modelClass: String = "card_detection" 40 | override val modelFrameworkVersion: Int = 1 41 | } 42 | } 43 | else -> { 44 | Log.d(Config.logTag, "No card detect available in assets") 45 | object : UpdatingModelWebFetcher(context) { 46 | override val defaultModelFileName: String = "UX.0.5.23.16.tflite" 47 | override val defaultModelVersion: String = "0.5.23.16" 48 | override val defaultModelHash: String = "ea51ca5c693a4b8733b1cf1a63557a713a13fabf0bcb724385077694e63a51a7" 49 | override val defaultModelHashAlgorithm: String = "SHA-256" 50 | override val modelClass: String = "card_detection" 51 | override val modelFrameworkVersion: Int = 1 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/ml/ExpiryDetectModelManager.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment.ml 2 | 3 | import android.content.Context 4 | import com.getbouncer.scan.framework.Fetcher 5 | import com.getbouncer.scan.payment.ModelManager 6 | 7 | @Deprecated( 8 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 9 | replaceWith = ReplaceWith("StripeCardScan"), 10 | ) 11 | object ExpiryDetectModelManager : ModelManager() { 12 | override fun getModelFetcher(context: Context): Fetcher = ExpiryDetect.ModelFetcher(context) 13 | } 14 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/ml/SSDOcrModelManager.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment.ml 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import com.getbouncer.scan.framework.Config 6 | import com.getbouncer.scan.framework.Fetcher 7 | import com.getbouncer.scan.framework.UpdatingModelWebFetcher 8 | import com.getbouncer.scan.framework.UpdatingResourceFetcher 9 | import com.getbouncer.scan.framework.assetFileExists 10 | import com.getbouncer.scan.payment.ModelManager 11 | 12 | private const val OCR_ASSET_FULL = "darknite_1_1_1_16.tflite" 13 | private const val OCR_ASSET_MINIMAL = "mb2_brex_metal_synthetic_svhnextra_epoch_3_5_98_8.tflite" 14 | 15 | @Deprecated( 16 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 17 | replaceWith = ReplaceWith("StripeCardScan"), 18 | ) 19 | object SSDOcrModelManager : ModelManager() { 20 | override fun getModelFetcher(context: Context): Fetcher = when { 21 | assetFileExists(context, OCR_ASSET_FULL) -> { 22 | Log.d(Config.logTag, "Full ocr available in assets") 23 | object : UpdatingResourceFetcher(context) { 24 | override val assetFileName: String = OCR_ASSET_FULL 25 | override val resourceModelVersion: String = "1.1.1.16" 26 | override val resourceModelHash: String = "8d8e3f79aa0783ab0cfa5c8d65d663a9da6ba99401efb2298aaaee387c3b00d6" 27 | override val resourceModelHashAlgorithm: String = "SHA-256" 28 | override val modelClass: String = "ocr" 29 | override val modelFrameworkVersion: Int = 1 30 | } 31 | } 32 | assetFileExists(context, OCR_ASSET_MINIMAL) -> { 33 | Log.d(Config.logTag, "Minimal ocr available in assets") 34 | object : UpdatingResourceFetcher(context) { 35 | override val assetFileName: String = OCR_ASSET_MINIMAL 36 | override val resourceModelVersion: String = "3.5.98.8" 37 | override val resourceModelHash: String = "a4739fa49caa3ff88e7ff1145c9334ee4cbf64354e91131d02d98d7bfd4c35cf" 38 | override val resourceModelHashAlgorithm: String = "SHA-256" 39 | override val modelClass: String = "ocr" 40 | override val modelFrameworkVersion: Int = 1 41 | } 42 | } 43 | else -> { 44 | Log.d(Config.logTag, "No ocr available in assets") 45 | object : UpdatingModelWebFetcher(context) { 46 | override val defaultModelFileName: String = "darknite_1_1_1_16.tflite" 47 | override val defaultModelVersion: String = "1.1.1.16" 48 | override val defaultModelHash: String = "8d8e3f79aa0783ab0cfa5c8d65d663a9da6ba99401efb2298aaaee387c3b00d6" 49 | override val defaultModelHashAlgorithm: String = "SHA-256" 50 | override val modelClass: String = "ocr" 51 | override val modelFrameworkVersion: Int = 1 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/ml/ssd/DetectionBox.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment.ml.ssd 2 | 3 | import android.graphics.RectF 4 | import androidx.annotation.RestrictTo 5 | 6 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 7 | @Deprecated( 8 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 9 | replaceWith = ReplaceWith("StripeCardScan"), 10 | ) 11 | data class DetectionBox( 12 | 13 | /** 14 | * The rectangle percentage of the original image. 15 | */ 16 | val rect: RectF, 17 | 18 | /** 19 | * Confidence value that the label applies to the rectangle. 20 | */ 21 | val confidence: Float, 22 | 23 | /** 24 | * The label for this box. 25 | */ 26 | val label: Int 27 | ) 28 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/ml/ssd/OcrPriorsGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment.ml.ssd 2 | 3 | import android.util.Size 4 | import com.getbouncer.scan.framework.ml.ssd.SizeAndCenter 5 | import com.getbouncer.scan.framework.ml.ssd.clampAll 6 | import com.getbouncer.scan.framework.ml.ssd.sizeAndCenter 7 | import kotlin.math.sqrt 8 | 9 | private const val NUMBER_OF_PRIORS = 3 10 | 11 | @Deprecated( 12 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 13 | replaceWith = ReplaceWith("StripeCardScan"), 14 | ) 15 | fun combinePriors(trainedImageSize: Size): Array { 16 | val priorsOne: Array = 17 | generatePriors( 18 | trainedImageSize = trainedImageSize, 19 | featureMapSize = Size(38, 24), 20 | shrinkage = Size(16, 16), 21 | boxSizeMin = 14F, 22 | boxSizeMax = 30F, 23 | aspectRatio = 3F 24 | ) 25 | 26 | val priorsTwo: Array = 27 | generatePriors( 28 | trainedImageSize = trainedImageSize, 29 | featureMapSize = Size(19, 12), 30 | shrinkage = Size(31, 31), 31 | boxSizeMin = 30F, 32 | boxSizeMax = 45F, 33 | aspectRatio = 3F 34 | ) 35 | 36 | return (priorsOne + priorsTwo).apply { forEach { it.clampAll(0F, 1F) } } 37 | } 38 | 39 | private fun generatePriors( 40 | trainedImageSize: Size, 41 | featureMapSize: Size, 42 | shrinkage: Size, 43 | boxSizeMin: Float, 44 | boxSizeMax: Float, 45 | aspectRatio: Float 46 | ): Array { 47 | val scaleWidth = trainedImageSize.width.toFloat() / shrinkage.width 48 | val scaleHeight = trainedImageSize.height.toFloat() / shrinkage.height 49 | val ratio = sqrt(aspectRatio) 50 | 51 | fun generatePrior(column: Int, row: Int, sizeFactor: Float, ratio: Float) = 52 | sizeAndCenter( 53 | centerX = (column + 0.5F) / scaleWidth, 54 | centerY = (row + 0.5F) / scaleHeight, 55 | width = sizeFactor / trainedImageSize.width, 56 | height = sizeFactor / trainedImageSize.height * ratio 57 | ) 58 | 59 | return Array(featureMapSize.width * featureMapSize.height * NUMBER_OF_PRIORS) { index -> 60 | val row = index / NUMBER_OF_PRIORS / featureMapSize.width 61 | val column = (index / NUMBER_OF_PRIORS) % featureMapSize.width 62 | when (index % NUMBER_OF_PRIORS) { 63 | 0 -> generatePrior(column, row, boxSizeMin, 1F) 64 | 1 -> generatePrior(column, row, sqrt(boxSizeMax * boxSizeMin), ratio) 65 | else -> generatePrior(column, row, boxSizeMin, ratio) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /scan-payment/src/main/java/com/getbouncer/scan/payment/ml/yolo/Yolo.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.payment.ml.yolo 2 | 3 | import android.graphics.RectF 4 | import android.util.Size 5 | import com.getbouncer.scan.framework.ml.ssd.softMax 6 | import com.getbouncer.scan.framework.util.indexOfMax 7 | import com.getbouncer.scan.payment.ml.ssd.DetectionBox 8 | import kotlin.math.exp 9 | 10 | @Deprecated( 11 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 12 | replaceWith = ReplaceWith("StripeCardScan"), 13 | ) 14 | fun sigmoid(x: Float): Float = (1.0f / (1.0f + exp((-x)))) 15 | 16 | /** 17 | * Takes a layer from the raw YOLO model output and performs post-processing on it, 18 | * returning a List 19 | */ 20 | @Deprecated( 21 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 22 | replaceWith = ReplaceWith("StripeCardScan"), 23 | ) 24 | fun processYoloLayer( 25 | layer: Array>, 26 | anchors: Array>, 27 | imageSize: Size, 28 | numClasses: Int, 29 | confidenceThreshold: Float 30 | ): List { 31 | val results = mutableListOf() 32 | for (i in layer.indices) for (j in layer[i].indices) for (k in 0..2) { 33 | val offset = (numClasses + 5) * k 34 | var confidence = sigmoid(layer[i][j][offset + 4]) 35 | val confidenceClasses = (0 until numClasses).map { layer[i][j][offset + 5 + it] }.toFloatArray() 36 | confidenceClasses.softMax() 37 | 38 | val objectId = confidenceClasses.indexOfMax() ?: continue 39 | val maxClass = confidenceClasses[objectId] 40 | confidence *= maxClass 41 | 42 | if (confidence > confidenceThreshold) { 43 | val x = (j + sigmoid(layer[i][j][offset])) / layer.size 44 | val y = (i + sigmoid(layer[i][j][offset + 1])) / layer.size 45 | val w = 46 | exp(layer[i][j][offset + 2]) * anchors[k].first / imageSize.width 47 | val h = 48 | exp(layer[i][j][offset + 3]) * anchors[k].second / imageSize.height 49 | val r = RectF( 50 | x - w / 2, 51 | y - h / 2, 52 | x + w / 2, 53 | y + h / 2 54 | ) 55 | results.add( 56 | DetectionBox( 57 | rect = r, 58 | confidence = confidence, 59 | label = objectId 60 | ) 61 | ) 62 | } 63 | } 64 | return results 65 | } 66 | -------------------------------------------------------------------------------- /scan-payment/src/main/res/raw/payment_card_types.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-payment/src/main/res/raw/payment_card_types.bin -------------------------------------------------------------------------------- /scan-ui/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /scan-ui/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-parcelize' 4 | 5 | android { 6 | compileSdkVersion 30 7 | buildToolsVersion '30.0.3' 8 | 9 | defaultConfig { 10 | minSdkVersion 21 11 | targetSdkVersion 30 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles 'consumer-rules.pro' 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | testOptions { 25 | unitTests.includeAndroidResources = true 26 | } 27 | 28 | lintOptions { 29 | enable "Interoperability" 30 | } 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | } 37 | 38 | dependencies { 39 | implementation fileTree(dir: 'libs', include: ['*.jar']) 40 | implementation project(":scan-framework") 41 | implementation project(':scan-camera') 42 | 43 | implementation "androidx.appcompat:appcompat:[1.3.0,1.3.1]" 44 | implementation "androidx.constraintlayout:constraintlayout:[2.0.4,2.1.0]" 45 | implementation "androidx.core:core-ktx:[1.3.1,1.6.0]" 46 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:[1.4.0,1.5.1]" 47 | } 48 | 49 | dependencies { 50 | testImplementation "androidx.test:core:1.4.0" 51 | testImplementation "androidx.test:runner:1.4.0" 52 | testImplementation "junit:junit:4.13.2" 53 | testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 54 | } 55 | 56 | dependencies { 57 | androidTestImplementation "androidx.test.ext:junit:1.1.3" 58 | androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" 59 | androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.5.30" 60 | } 61 | 62 | apply from: 'deploy.gradle' 63 | -------------------------------------------------------------------------------- /scan-ui/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/scan-ui/consumer-rules.pro -------------------------------------------------------------------------------- /scan-ui/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /scan-ui/src/androidTest/java/com/getbouncer/scan/ui/DebugOverlayTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.ui 2 | 3 | import android.graphics.RectF 4 | import android.util.Size 5 | import androidx.test.filters.SmallTest 6 | import org.junit.Test 7 | import kotlin.test.assertEquals 8 | 9 | class DebugOverlayTest { 10 | 11 | @Test 12 | @SmallTest 13 | fun scaleRect() { 14 | assertEquals(RectF(2F, 4F, 6F, 8F), RectF(0.05F, 0.10F, 0.15F, 0.20F).scaled(Size(40, 40))) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scan-ui/src/androidTest/java/com/getbouncer/scan/ui/util/ViewExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.ui.util 2 | 3 | import androidx.test.filters.SmallTest 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import com.getbouncer.scan.ui.test.R 6 | import org.junit.Test 7 | import kotlin.test.assertEquals 8 | 9 | class ViewExtensionsTest { 10 | private val testContext = InstrumentationRegistry.getInstrumentation().context 11 | 12 | @Test 13 | @SmallTest 14 | fun getColorByRes_matches() { 15 | val color = testContext.getColorByRes(R.color.testColor) 16 | val alpha = color shr 24 and 0xFF 17 | val red = color shr 16 and 0xFF 18 | val green = color shr 8 and 0xFF 19 | val blue = color and 0xFF 20 | 21 | assertEquals(0xA1, alpha) 22 | assertEquals(0x1E, red) 23 | assertEquals(0x90, green) 24 | assertEquals(0xFF, blue) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scan-ui/src/androidTest/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #A11E90FF 4 | 5 | -------------------------------------------------------------------------------- /scan-ui/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /scan-ui/src/main/java/com/getbouncer/scan/ui/ScanFlow.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.ui 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Rect 6 | import androidx.lifecycle.LifecycleOwner 7 | import com.getbouncer.scan.camera.CameraPreviewImage 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.flow.Flow 10 | 11 | /** 12 | * A flow for scanning something. This manages the callbacks and lifecycle of the flow. 13 | */ 14 | @Deprecated( 15 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 16 | replaceWith = ReplaceWith("StripeCardScan"), 17 | ) 18 | interface ScanFlow { 19 | 20 | /** 21 | * Start the image processing flow for scanning a card. 22 | * 23 | * @param context: The context used to download analyzers if needed 24 | * @param imageStream: The flow of images to process 25 | * @param viewFinder: The location of the view finder in the previewSize 26 | * @param lifecycleOwner: The activity that owns this flow. The flow will pause if the activity 27 | * is paused 28 | * @param coroutineScope: The coroutine scope used to run async tasks for this flow 29 | */ 30 | fun startFlow( 31 | context: Context, 32 | imageStream: Flow>, 33 | viewFinder: Rect, 34 | lifecycleOwner: LifecycleOwner, 35 | coroutineScope: CoroutineScope 36 | ) 37 | 38 | /** 39 | * In the event that the scan cannot complete, halt the flow to halt analyzers and free up CPU and memory. 40 | */ 41 | fun cancelFlow() 42 | } 43 | -------------------------------------------------------------------------------- /scan-ui/src/main/java/com/getbouncer/scan/ui/ViewFinderBackground.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.ui 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.graphics.PorterDuff 7 | import android.graphics.PorterDuffXfermode 8 | import android.graphics.Rect 9 | import android.os.Build 10 | import android.util.AttributeSet 11 | import android.view.View 12 | import androidx.annotation.ColorInt 13 | import kotlin.math.roundToInt 14 | 15 | /** 16 | * This class draws a background with a hole in the middle of it. 17 | */ 18 | @Deprecated( 19 | message = "Replaced by stripe card scan. See https://github.com/stripe/stripe-android/tree/master/stripecardscan", 20 | replaceWith = ReplaceWith("StripeCardScan"), 21 | ) 22 | class ViewFinderBackground(context: Context, attrs: AttributeSet? = null) : View(context, attrs) { 23 | private var viewFinderRect: Rect? = null 24 | private var onDrawListener: (() -> Unit)? = null 25 | 26 | fun setViewFinderRect(viewFinderRect: Rect) { 27 | this.viewFinderRect = viewFinderRect 28 | requestLayout() 29 | } 30 | 31 | fun clearViewFinderRect() { 32 | this.viewFinderRect = null 33 | } 34 | 35 | override fun setBackgroundColor(@ColorInt color: Int) { 36 | paintBackground.color = color 37 | requestLayout() 38 | } 39 | 40 | fun getBackgroundLuminance(): Int { 41 | val color = paintBackground.color 42 | val r = (color shr 16 and 0xff) / 255F 43 | val g = (color shr 8 and 0xff) / 255F 44 | val b = (color and 0xff) / 255F 45 | 46 | return ((0.2126F * r + 0.7152F * g + 0.0722F * b) * 255F).roundToInt() 47 | } 48 | 49 | fun setOnDrawListener(onDrawListener: () -> Unit) { 50 | this.onDrawListener = onDrawListener 51 | } 52 | 53 | fun clearOnDrawListener() { 54 | this.onDrawListener = null 55 | } 56 | 57 | private val theme = context.theme 58 | private val attributes = theme.obtainStyledAttributes(attrs, R.styleable.ViewFinderBackground, 0, 0) 59 | private val backgroundColor = 60 | attributes.getColor( 61 | R.styleable.ViewFinderBackground_backgroundColor, 62 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 63 | resources.getColor(R.color.bouncerNotFoundBackground, theme) 64 | } else { 65 | @Suppress("deprecation") 66 | resources.getColor(R.color.bouncerNotFoundBackground) 67 | } 68 | ) 69 | 70 | private var paintBackground = Paint(Paint.ANTI_ALIAS_FLAG).apply { 71 | color = backgroundColor 72 | style = Paint.Style.FILL 73 | } 74 | 75 | private val paintWindow = Paint(Paint.ANTI_ALIAS_FLAG).apply { 76 | xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) 77 | style = Paint.Style.FILL 78 | } 79 | 80 | init { 81 | setLayerType(LAYER_TYPE_HARDWARE, null) 82 | } 83 | 84 | override fun onDraw(canvas: Canvas) { 85 | super.onDraw(canvas) 86 | canvas.drawPaint(paintBackground) 87 | 88 | val viewFinderRect = this.viewFinderRect 89 | if (viewFinderRect != null) { 90 | canvas.drawRect(viewFinderRect, paintWindow) 91 | } 92 | 93 | val onDrawListener = this.onDrawListener 94 | if (onDrawListener != null) { 95 | onDrawListener() 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_camera_swap_dark.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 19 | 20 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_camera_swap_light.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 19 | 20 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_card_background_correct.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 30 | 31 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_card_background_found.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 30 | 31 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_card_background_not_found.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 30 | 31 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_card_background_wrong.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 30 | 31 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_card_border_correct.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 26 | 27 | 28 | 29 | 30 | 31 | 38 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_card_border_found.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 24 | 25 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_card_border_found_long.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 28 | 29 | 30 | 31 | 32 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_card_border_not_found.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 24 | 25 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_card_border_wrong.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 26 | 27 | 28 | 29 | 30 | 31 | 38 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_close_button_dark.xml: -------------------------------------------------------------------------------- 1 | 6 | 15 | 16 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_close_button_light.xml: -------------------------------------------------------------------------------- 1 | 6 | 15 | 16 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_flash_off_dark.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 18 | 19 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_flash_off_light.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 18 | 19 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_flash_on_dark.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 19 | 20 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_flash_on_light.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 19 | 20 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_lock_dark.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 31 | 32 | 41 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/drawable/bouncer_lock_light.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 31 | 32 | 41 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Scan Your Card 4 | Your card info is secure 5 | Card doesn\'t match 6 | 7 | Please allow camera access to scan your card 8 | OK 9 | Cancel 10 | 11 | Camera Problem 12 | The camera failed to turn on 13 | Permission was denied when turning on the camera 14 | This device does not support the required camera features 15 | Close 16 | 17 | Network Problem 18 | Sorry, this API key is not valid. 19 | OK 20 | 21 | cardscan.io logo 22 | lock icon 23 | card scanning window 24 | card debug window 25 | Close 26 | Torch 27 | Swap Camera 28 | card view finder 29 | 30 | -------------------------------------------------------------------------------- /scan-ui/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /scan-ui/src/test/java/com/getbouncer/scan/ui/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.getbouncer.scan.ui 2 | 3 | import org.junit.Test 4 | import kotlin.test.assertEquals 5 | 6 | class ExampleUnitTest { 7 | 8 | @Test 9 | fun addition_isCorrect() { 10 | assertEquals(4, 2 + 2) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='cardscan-android' 2 | include ':cardscan-demo' 3 | include ':cardscan-ui' 4 | include ':scan-camera' 5 | include ':scan-camera2' 6 | include ':scan-camerax' 7 | include ':scan-framework' 8 | include ':scan-payment' 9 | include ':scan-payment-full' 10 | include ':scan-payment-minimal' 11 | include ':scan-ui' 12 | include ':tensorflow-lite' 13 | include ':tensorflow-lite-arm-only' 14 | -------------------------------------------------------------------------------- /tensorflow-lite-arm-only/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.asc 3 | -------------------------------------------------------------------------------- /tensorflow-lite-arm-only/build.gradle: -------------------------------------------------------------------------------- 1 | configurations.maybeCreate("default") 2 | artifacts.add("default", file('tensorflow-lite-all-models-arm-only.aar')) 3 | 4 | apply from: "deploy.gradle" 5 | -------------------------------------------------------------------------------- /tensorflow-lite-arm-only/tensorflow-lite-all-models-arm-only.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/tensorflow-lite-arm-only/tensorflow-lite-all-models-arm-only.aar -------------------------------------------------------------------------------- /tensorflow-lite/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.asc 3 | -------------------------------------------------------------------------------- /tensorflow-lite/build.gradle: -------------------------------------------------------------------------------- 1 | configurations.maybeCreate("default") 2 | artifacts.add("default", file('tensorflow-lite-all-models.aar')) 3 | 4 | apply from: "deploy.gradle" 5 | -------------------------------------------------------------------------------- /tensorflow-lite/tensorflow-lite-all-models.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbouncer/cardscan-android/bf490afe479ec5d2b4c3276e74350fa26e82daa8/tensorflow-lite/tensorflow-lite-all-models.aar --------------------------------------------------------------------------------