├── .circleci ├── config.yml └── scripts │ ├── checksum.sh │ ├── collect-artifacts.sh │ └── test-firebase.sh ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── DiagramArchitecture.png ├── LICENSE ├── Logo.png ├── LogoLarge.png ├── README.md ├── SampleApp.gif ├── app ├── .gitignore ├── build.gradle └── src │ ├── androidTest │ └── java │ │ └── uk │ │ └── co │ │ └── brightec │ │ └── kbarcode │ │ └── app │ │ ├── testutil │ │ └── SingleFragmentActivity.kt │ │ └── viewfinder │ │ └── ViewfinderScreenTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── uk │ │ │ └── co │ │ │ └── brightec │ │ │ └── kbarcode │ │ │ └── app │ │ │ ├── Application.kt │ │ │ ├── MainActivity.kt │ │ │ ├── ProgrammaticActivity.kt │ │ │ ├── XmlActivity.kt │ │ │ ├── XmlJavaActivity.java │ │ │ ├── camerax │ │ │ ├── BarcodeAnalyzer.kt │ │ │ └── CameraXActivity.kt │ │ │ ├── util │ │ │ └── OpenForTesting.kt │ │ │ └── viewfinder │ │ │ ├── Resource.kt │ │ │ ├── ViewfinderActivity.kt │ │ │ ├── ViewfinderFragment.kt │ │ │ └── ViewfinderViewModel.kt │ └── res │ │ ├── drawable │ │ ├── ic_launcher_foreground.xml │ │ └── img_viewfinder_black_125dp.xml │ │ ├── layout │ │ ├── activity_camerax.xml │ │ ├── activity_main.xml │ │ ├── activity_programmatic.xml │ │ ├── activity_viewfinder.xml │ │ ├── activity_xml.xml │ │ └── fragment_viewfinder.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 │ │ └── xml │ │ └── backup_descriptor.xml │ └── test │ └── resources │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── build.gradle ├── config ├── misc │ ├── apknaming.gradle │ ├── proguard-rules.pro │ └── proguardTest-rules.pro └── quality │ ├── checkstyle │ └── checkstyle-config.xml │ ├── detekt │ └── detekt-config.yml │ ├── ktlint │ └── libs │ │ └── ktlint-rules-0.0.10.jar │ ├── lint.xml │ ├── pmd │ ├── pmd-ruleset.xml │ └── pmd-test-ruleset.xml │ ├── quality.gradle │ └── xmlchecker │ └── libs │ └── xmlcheck-2.0.3.jar ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kbarcode ├── .gitignore ├── build.gradle └── src │ ├── androidTest │ └── java │ │ └── uk │ │ └── co │ │ └── brightec │ │ └── kbarcode │ │ ├── BarcodeScannerAndroidTest.kt │ │ ├── BarcodeViewTest.kt │ │ ├── extension │ │ └── PointTest.kt │ │ └── processor │ │ ├── BarcodeImageProcessorTest.kt │ │ └── sort │ │ └── CentralBarcodeComparatorTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── uk │ │ │ └── co │ │ │ └── brightec │ │ │ └── kbarcode │ │ │ ├── Barcode.kt │ │ │ ├── BarcodeScanner.kt │ │ │ ├── BarcodeView.kt │ │ │ ├── KBarcode.kt │ │ │ ├── Options.kt │ │ │ ├── camera │ │ │ ├── Camera2Source.kt │ │ │ ├── CameraException.kt │ │ │ ├── FrameMetadata.kt │ │ │ ├── OnCameraErrorListener.java │ │ │ └── OnCameraReadyListener.kt │ │ │ ├── extension │ │ │ ├── _Int.kt │ │ │ └── _Point.kt │ │ │ ├── model │ │ │ ├── Address.kt │ │ │ ├── CalendarDateTime.kt │ │ │ ├── CalendarEvent.kt │ │ │ ├── ContactInfo.kt │ │ │ ├── DrivingLicense.kt │ │ │ ├── Email.kt │ │ │ ├── GeoPoint.kt │ │ │ ├── PersonName.kt │ │ │ ├── Phone.kt │ │ │ ├── Sms.kt │ │ │ ├── UrlBookmark.kt │ │ │ └── WiFi.kt │ │ │ ├── processor │ │ │ ├── BarcodeImageProcessor.kt │ │ │ ├── OnBarcodeListener.java │ │ │ ├── OnBarcodesListener.java │ │ │ ├── base │ │ │ │ ├── ImageProcessor.kt │ │ │ │ └── ImageProcessorBase.kt │ │ │ └── sort │ │ │ │ ├── BarcodeComparator.kt │ │ │ │ └── CentralBarcodeComparator.kt │ │ │ └── util │ │ │ ├── Constants.kt │ │ │ └── OpenForTesting.kt │ └── res │ │ └── values │ │ └── attrs.xml │ └── test │ ├── java │ └── uk │ │ └── co │ │ └── brightec │ │ └── kbarcode │ │ ├── BarcodeScannerTest.kt │ │ ├── KBarcodeScannerTest.kt │ │ ├── camera │ │ └── Camera2SourceTest.kt │ │ └── processor │ │ └── base │ │ └── ImageProcessorBaseTest.kt │ └── resources │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── proguard-rules.pro ├── proguardTest-rules.pro ├── scripts └── publish-mavencentral.gradle └── settings.gradle /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | commands: 4 | check_halt: 5 | description: Check whether to continue this job 6 | steps: 7 | - attach_workspace: 8 | at: /tmp/workspace 9 | - run: 10 | command: | 11 | if [[ `cat /tmp/workspace/persist/job-halt` == 1 ]]; then 12 | echo "Job not required" 13 | circleci step halt 14 | fi 15 | name: Checking whether to halt this job 16 | checksum: 17 | description: Generate cache key as checksum 18 | steps: 19 | - run: 20 | command: ./.circleci/scripts/checksum.sh /tmp/checksum.txt 21 | name: Generate cache key 22 | download_google_services: 23 | description: Download google-services.json 24 | steps: 25 | - run: 26 | command: | 27 | echo $GOOGLE_SERVICES | base64 -di > app/google-services.json 28 | name: Download google-services.json 29 | load_gpg_key: 30 | description: Loads the Base64 encoded GPG key into a file 31 | steps: 32 | - run: 33 | name: Load GPG key 34 | command: echo $GPG_KEY_CONTENTS | base64 -d > $SIGNING_SECRET_KEY_RING_FILE 35 | 36 | jobs: 37 | nightly_check: 38 | docker: 39 | - image: circleci/android:api-29-node 40 | steps: 41 | - run: 42 | command: | 43 | HTTP_LAST_BUILD=$(curl --write-out "HTTPSTATUS:%{http_code}" \ 44 | --request GET \ 45 | "$URL_BASE_KEYVALUE/$CIRCLE_PROJECT_REPONAME/$LAST_BUILD_TOKEN") 46 | LAST_BUILD=$(echo $HTTP_LAST_BUILD | sed -e 's/HTTPSTATUS\:.*//g') 47 | STATUS_LAST_BUILD=$(echo $HTTP_LAST_BUILD | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') 48 | if [ $STATUS_LAST_BUILD != 200 ]; then 49 | echo -e "Fetching key value for last build failed.\nStatus: $STATUS_LAST_BUILD" 50 | exit 1 51 | else 52 | echo "export LAST_BUILD=$LAST_BUILD" >> $BASH_ENV 53 | fi 54 | name: Get last build SHA1 55 | - run: 56 | command: | 57 | HTTP_SAVE_LAST_BUILD=$(curl --write-out "HTTPSTATUS:%{http_code}" \ 58 | --request POST \ 59 | "$URL_BASE_KEYVALUE/$CIRCLE_PROJECT_REPONAME/$LAST_BUILD_TOKEN/$CIRCLE_SHA1") 60 | STATUS_SAVE_LAST_BUILD=$(echo $HTTP_SAVE_LAST_BUILD | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') 61 | if [ $STATUS_SAVE_LAST_BUILD != 200 ]; then 62 | echo -e "Saving key value for last build failed.\nStatus: $STATUS_SAVE_LAST_BUILD" 63 | exit 1 64 | fi 65 | name: Save current SHA1 66 | - checkout 67 | - run: 68 | command: | 69 | mkdir -p workspace/persist 70 | if [ "$LAST_BUILD" != '' ] && [ $LAST_BUILD == $CIRCLE_SHA1 ]; then 71 | echo "Job not required" 72 | echo 1 > workspace/persist/job-halt 73 | fi 74 | name: Check if deployment is required 75 | - persist_to_workspace: 76 | paths: 77 | - persist 78 | root: workspace 79 | working_directory: ~/workspace 80 | 81 | tests: 82 | docker: 83 | - image: circleci/android:api-29-node 84 | resource_class: medium+ 85 | steps: 86 | - check_halt 87 | - checkout 88 | - download_google_services 89 | - checksum 90 | - restore_cache: 91 | key: gradle-{{ arch }}-{{ checksum "/tmp/checksum.txt" }} 92 | - run: 93 | name: Download Dependencies 94 | command: ./gradlew androidDependencies 95 | - save_cache: 96 | key: gradle-{{ arch }}-{{ checksum "/tmp/checksum.txt" }} 97 | paths: 98 | - ~/.gradle 99 | - run: 100 | command: | 101 | if ./gradlew tasks --all | grep -q 'shared:check'; then 102 | ./gradlew check -x lint -x test -x shared:check 103 | else 104 | ./gradlew check -x lint -x test 105 | fi 106 | name: Run checks 107 | - run: 108 | command: | 109 | ./gradlew lintReleaseOnly 110 | name: Run lint 111 | - run: 112 | command: ./gradlew test 113 | name: Run Unit Tests 114 | no_output_timeout: 30m 115 | - run: 116 | command: ./.circleci/scripts/collect-artifacts.sh /tmp/artifacts 117 | name: Collect artifacts 118 | when: always 119 | - store_artifacts: 120 | destination: /artifacts 121 | path: /tmp/artifacts 122 | - store_test_results: 123 | path: app/build/test-results 124 | working_directory: ~/workspace 125 | 126 | tests_android: 127 | docker: 128 | - image: circleci/android:api-29-node 129 | resource_class: medium+ 130 | steps: 131 | - check_halt 132 | - checkout 133 | - download_google_services 134 | - checksum 135 | - restore_cache: 136 | key: gradle-{{ arch }}-{{ checksum "/tmp/checksum.txt" }} 137 | - run: 138 | command: ./gradlew androidDependencies 139 | name: Download Dependencies 140 | - save_cache: 141 | key: gradle-{{ arch }}-{{ checksum "/tmp/checksum.txt" }} 142 | paths: 143 | - ~/.gradle 144 | - run: 145 | command: | 146 | if [ ! -z "$COMMAND_CREATE_APKS_TEST" ]; then 147 | echo "Running tests for: COMMAND_CREATE_APKS_TEST" 148 | eval "$COMMAND_CREATE_APKS_TEST" 149 | ./.circleci/scripts/test-firebase.sh $CIRCLE_PROJECT_REPONAME $CIRCLE_BUILD_NUM "/tmp/firebase_test_results/main" $FB_TEST_LAB_BUCKET 150 | fi 151 | name: Assemble and run on Firebase Test Lab 152 | no_output_timeout: 30m 153 | - store_artifacts: 154 | destination: /firebase_test_results 155 | path: /tmp/firebase_test_results 156 | working_directory: ~/workspace 157 | 158 | dist_library: 159 | docker: 160 | - image: circleci/android:api-29-node 161 | resource_class: medium+ 162 | steps: 163 | - checkout 164 | - download_google_services 165 | - load_gpg_key 166 | - checksum 167 | - restore_cache: 168 | key: gradle-{{ arch }}-{{ checksum "/tmp/checksum.txt" }} 169 | - run: 170 | name: Download Dependencies 171 | command: ./gradlew androidDependencies 172 | - save_cache: 173 | paths: 174 | - ~/.gradle 175 | key: gradle-{{ arch }}-{{ checksum "/tmp/checksum.txt" }} 176 | - run: 177 | name: Assemble library and distribute 178 | command: | 179 | ./gradlew kbarcode:assembleRelease 180 | ./gradlew kbarcode:androidSourcesJar 181 | ./gradlew kbarcode:publishReleasePublicationToMavenCentralRepository 182 | - store_artifacts: 183 | path: kbarcode/build/outputs/aar 184 | destination: apk 185 | - store_artifacts: 186 | path: kbarcode/build/outputs/mapping 187 | destination: mapping 188 | working_directory: ~/workspace 189 | 190 | parameters: 191 | run_checks: 192 | default: true 193 | type: boolean 194 | run_deploy_library: 195 | default: false 196 | type: boolean 197 | 198 | workflows: 199 | checks: 200 | jobs: 201 | - tests: 202 | context: Android 203 | - tests_android: 204 | context: Android 205 | requires: 206 | - tests 207 | when: << pipeline.parameters.run_checks >> 208 | deploy_library: 209 | jobs: 210 | - dist_library 211 | when: << pipeline.parameters.run_deploy_library >> 212 | release: 213 | jobs: 214 | - dist_library: 215 | filters: 216 | branches: 217 | ignore: /.*/ 218 | tags: 219 | only: /^v.*/ 220 | version: 2 221 | -------------------------------------------------------------------------------- /.circleci/scripts/checksum.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | RESULT_FILE=$1 3 | 4 | if [ -f $RESULT_FILE ]; then 5 | rm $RESULT_FILE 6 | fi 7 | touch $RESULT_FILE 8 | 9 | checksum_file() { 10 | echo $(openssl md5 $1 | awk '{print $2}') 11 | } 12 | 13 | FILES=() 14 | while read -r -d ''; do 15 | FILES+=("$REPLY") 16 | done < <(find . -name 'build.gradle' -type f -print0) 17 | 18 | # Loop through files and append MD5 to result file 19 | for FILE in ${FILES[@]}; do 20 | echo $(checksum_file $FILE) >> $RESULT_FILE 21 | done 22 | # Now sort the file so that it is 23 | sort $RESULT_FILE -o $RESULT_FILE 24 | -------------------------------------------------------------------------------- /.circleci/scripts/collect-artifacts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Name variables 4 | ARTIFACTS_DIR=$1 5 | 6 | # Safety checks 7 | if [ -z "$ARTIFACTS_DIR" ]; then 8 | echo "ARTIFACTS_DIR variable not supplied. Exiting." 9 | exit 1 10 | fi 11 | 12 | # Find and copy all reports 13 | find . -path "*build/reports" | while read REPORTS_PATH; do 14 | FOLDER=$(echo $REPORTS_PATH | cut -d "/" -f 2) 15 | DIR="$ARTIFACTS_DIR/$FOLDER" 16 | mkdir -p $DIR 17 | cp -r $REPORTS_PATH $DIR 18 | done 19 | 20 | # Find and copy all test results 21 | find . -path "*build/test-results" | while read TEST_RESULTS_PATH; do 22 | FOLDER=$(echo ${TEST_RESULTS_PATH} | cut -d "/" -f 2) 23 | DIR="$ARTIFACTS_DIR/$FOLDER" 24 | mkdir -p $DIR 25 | cp -r $TEST_RESULTS_PATH $DIR 26 | done 27 | -------------------------------------------------------------------------------- /.circleci/scripts/test-firebase.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Name variables 4 | PROJECT_NAME=$1 5 | BUILD_NO=$2 6 | TEST_DIR=$3 7 | RESULTS_BUCKET=$4 8 | 9 | # Safety checks 10 | if [ -z "$PROJECT_NAME" ]; then 11 | echo "PROJECT_NAME variable not supplied. Exiting." 12 | exit 1 13 | fi 14 | if [ -z "$BUILD_NO" ]; then 15 | echo "BUILD_NO variable not supplied. Exiting." 16 | exit 1 17 | fi 18 | if [ -z "$TEST_DIR" ]; then 19 | echo "TEST_DIR variable not supplied. Exiting." 20 | exit 1 21 | fi 22 | if [ -z "$RESULTS_BUCKET" ]; then 23 | echo "RESULTS_BUCKET variable not supplied. Exiting." 24 | exit 1 25 | fi 26 | 27 | # Install Flank 28 | wget --quiet https://github.com/TestArmada/flank/releases/download/v8.1.0/flank.jar -O ./flank.jar 29 | 30 | # Get the apk path 31 | APK_PATH=$(find . -path "*.apk" ! -path "*unaligned.apk" ! -path "*Test*.apk" -print -quit) 32 | 33 | # Get the app test apk path 34 | APK_APP_TEST_PATH=$(find . -path "*app*Test*.apk" -print -quit) 35 | 36 | # Setup GCloud Auth 37 | echo "$GCP_SERVICE_KEY" > "$HOME/gcp-service-key.json" 38 | export GOOGLE_APPLICATION_CREDENTIALS="$HOME/gcp-service-key.json" 39 | 40 | # Create Flank config file 41 | cat < "$HOME/flank.yml" 42 | gcloud: 43 | app: $APK_PATH 44 | test: $APK_APP_TEST_PATH 45 | type: instrumentation 46 | device: 47 | - model: NexusLowRes 48 | version: 28 49 | locale: en 50 | orientation: portrait 51 | timeout: 30m 52 | results-bucket: $RESULTS_BUCKET 53 | results-dir: $PROJECT_NAME/$BUILD_NO 54 | flank: 55 | max-test-shards: -1 56 | shard-time: 120 57 | smart-flank-gcs-path: gs://$RESULTS_BUCKET/$PROJECT_NAME/flank/android.xml 58 | project: $FB_TEST_LAB_PROJECT_ID 59 | local-result-dir: $TEST_DIR 60 | files-to-download: 61 | - .*\.mp4 62 | - .*\.xml 63 | EOF 64 | 65 | count=$(find . -path "*Test*.apk" ! -path "*app*Test*.apk" | wc -l) 66 | # shellcheck disable=SC2004 67 | if (( $count > 0 )); then 68 | echo " additional-app-test-apks:" >> "$HOME/flank.yml" 69 | 70 | # Loop through all additional test apks and append them to Flank config 71 | while IFS= read -r -d '' TEST_APK_PATH; do 72 | echo " - test: $TEST_APK_PATH" >> "$HOME/flank.yml" 73 | done < <(find . -path "*Test*.apk" ! -path "*app*Test*.apk" -print0) 74 | fi 75 | 76 | # Run Flank 77 | java -jar ./flank.jar android run --config="$HOME/flank.yml" 78 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] Title" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Smartphone (please complete the following information):** 27 | - Device: [e.g. iPhone6] 28 | - OS: [e.g. iOS8.1] 29 | - Browser [e.g. stock browser, safari] 30 | - Version [e.g. 22] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE] Title" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | /app/google-services.json 10 | -------------------------------------------------------------------------------- /DiagramArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightec/KBarcode/b5372973c0a8fa90cb90203dc3773b0891223330/DiagramArchitecture.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brightec 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightec/KBarcode/b5372973c0a8fa90cb90203dc3773b0891223330/Logo.png -------------------------------------------------------------------------------- /LogoLarge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightec/KBarcode/b5372973c0a8fa90cb90203dc3773b0891223330/LogoLarge.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KBarcode 2 | 3 | -------------------------------------- 4 | 5 | **:warning: Signing key revoked** 6 | 7 | The key used to sign the maven central distributions for this library has been revoked. If you have issues importing them, we suggest either migrating to CameraX (see deprecation notice below) or importing the source code of this library directly into you project. 8 | 9 | -------------------------------------- 10 | 11 | **:warning: Deprecated**: This library is no longer maintained 12 | 13 | This library was written before the first stable release of the CameraX library. It was intended to provide a thorough and high quality implementation of the camera2 APIs. Since the launch of CameraX and it's ongoing development, we would recommend using that library for a barcode scanning use case. 14 | 15 | To help you migrate we already have an example of a CameraX implementation within the demo app included in this repo. See also [CameraX section](https://github.com/brightec/KBarcode/wiki/Sample#camerax-example) of our wiki 16 | 17 | -------------------------------------- 18 | 19 |
20 |
21 |
22 | 23 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/uk.co.brightec.kbarcode/kbarcode/badge.png?style=for-the-badge)](https://maven-badges.herokuapp.com/maven-central/uk.co.brightec.kbarcode/kbarcode) 24 | 25 | A library to help implement barcode scanning. 26 | 27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 | ## Why? 36 | 37 | Another barcode library. Yawn. 38 | 39 | We can understand why you may think that, but there are some key reasons we decided to write a new barcode library. 40 | 41 | - **Quality** We want this library to be a high quality production ready library. 42 | - **Camera2** Many barcode libraries still use camera1 API's. These are now deprecated and although unlikely to be removed, you can get better performance and stability from camera2. You are also safe in the knowledge that Android will work to fix issues, and the library will have more longevity. 43 | - **MLKit** This library uses Google MLKit to process the frames and return barcodes. The Google team are committed to these API's and continue to work to improve them. 44 | - **Tested** We want this library to have tests. It's surprising how many don't. 45 | - **Simple** We want the implementation to be simple, but not try to hide away too much of the complexity of the task. 46 | 47 | ## Download 48 | 49 | ``` 50 | implementation 'uk.co.brightec.kbarcode:kbarcode:$version' 51 | ``` 52 | 53 | ## Releases 54 | 55 | See the [releases](https://github.com/brightec/KBarcode/releases) section for details about each release and any migration steps required. 56 | 57 | **Releases requiring migration:** `1.0.2`, `1.0.3`, `1.3.0` 58 | 59 | **Releases with behaviour changes:** `1.1.0`, `1.2.3` 60 | 61 | ## Wiki 62 | 63 | For a detailed look at the library and a full get started guide checkout the [wiki](https://github.com/brightec/KBarcode/wiki) 64 | 65 | ## Community 66 | 67 | We welcome community involvement with this library. We want this library to be useful for others, and of a high production quality. 68 | 69 | ### Issues 70 | 71 | Please do raise issues if you find problems with the library, sample or its documentation. We have provided a template to use. 72 | 73 | ### Pull Requests 74 | 75 | If you find an issue, why not try to fix it and create a pull request. We run CI checks on every pull request which must pass. 76 | To run these locally 77 | ``` 78 | ./gradlew check connectedAndroidTest 79 | ``` 80 | 81 | This will run our [code standards](https://github.com/brightec/Guidelines_Android) checks, lint, tests and instrumented tests. 82 | 83 | If you're keen to help, why not fix someone else's issue. 84 | 85 | ### Feature Requests 86 | 87 | You can submit feature requests as issues. As mentioned above we want this library to be simple, high quality and production ready. We therefore may be selective about which features we wish to include in order to achieve these goals. 88 | 89 | Before fully coding a feature, why not raise an issue to start a discussion with us. 90 | 91 | ## License 92 | 93 | See [license](LICENSE) 94 | 95 | ## Author 96 | 97 | Alistair Sykes - [Github](https://github.com/alistairsykes) [Medium](https://medium.com/@alistairsykes) [Twitter](https://twitter.com/SykesAlistair) 98 | 99 | This library is maintained by the [Brightec](https://www.brightec.co.uk/) team 100 | -------------------------------------------------------------------------------- /SampleApp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightec/KBarcode/b5372973c0a8fa90cb90203dc3773b0891223330/SampleApp.gif -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | apply plugin: "kotlin-android" 3 | apply plugin: "kotlin-allopen" 4 | apply plugin: "com.google.gms.google-services" 5 | apply from: "$project.rootDir/config/quality/quality.gradle" 6 | apply from: "$project.rootDir/config/misc/apknaming.gradle" 7 | 8 | allOpen { 9 | annotation "uk.co.brightec.kbarcode.app.util.OpenClass" 10 | } 11 | 12 | import java.text.SimpleDateFormat 13 | 14 | static def buildTime() { 15 | def df = new SimpleDateFormat("dd.MM.yyyy HH.mm") 16 | df.setTimeZone(TimeZone.getTimeZone("UTC+01:00")) 17 | return df.format(new Date()) 18 | } 19 | 20 | static def getBuildNumber() { 21 | return System.getenv("CIRCLE_BUILD_NUM") as Integer ?: 1 22 | } 23 | 24 | android { 25 | compileSdkVersion 30 26 | 27 | defaultConfig { 28 | applicationId "uk.co.brightec.kbarcode.app" 29 | minSdkVersion 21 30 | targetSdkVersion 30 31 | versionCode getBuildNumber() 32 | versionName "1.0.0" 33 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 34 | } 35 | 36 | buildFeatures { 37 | viewBinding true 38 | } 39 | 40 | lintOptions { 41 | lintConfig file("$project.rootDir/config/quality/lint.xml") 42 | } 43 | 44 | compileOptions { 45 | sourceCompatibility 1.8 46 | targetCompatibility 1.8 47 | } 48 | 49 | kotlinOptions { 50 | jvmTarget = "1.8" 51 | } 52 | 53 | buildTypes { 54 | debug { 55 | minifyEnabled false 56 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "$project.rootDir/config/misc/proguard-rules.pro", "$project.rootDir/proguard-rules.pro" 57 | testProguardFiles getDefaultProguardFile("proguard-android.txt"), "$project.rootDir/config/misc/proguardTest-rules.pro", "$project.rootDir/proguardTest-rules.pro" 58 | versionNameSuffix " (DEBUG ${buildTime()})" 59 | } 60 | 61 | release { 62 | minifyEnabled true 63 | shrinkResources true 64 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "$project.rootDir/config/misc/proguard-rules.pro", "$project.rootDir/proguard-rules.pro" 65 | testProguardFiles getDefaultProguardFile("proguard-android.txt"), "$project.rootDir/config/misc/proguardTest-rules.pro", "$project.rootDir/proguardTest-rules.pro" 66 | versionNameSuffix " (#${getBuildNumber()})" 67 | } 68 | } 69 | } 70 | 71 | dependencies { 72 | // Use this in your app 73 | // implementation "uk.co.brightec.kbarcode:kbarcode:$version" 74 | // We use this for developing the library 75 | implementation project(":kbarcode") 76 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 77 | implementation "androidx.constraintlayout:constraintlayout:2.0.4" 78 | implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" 79 | implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1" 80 | implementation "com.google.android.material:material:1.3.0" 81 | implementation platform("com.google.firebase:firebase-bom:28.1.0") 82 | implementation "com.google.firebase:firebase-core" 83 | // For CameraX example only 84 | implementation "androidx.camera:camera-camera2:1.0.0" 85 | implementation "androidx.camera:camera-lifecycle:1.0.0" 86 | implementation "androidx.camera:camera-view:1.0.0-alpha25" 87 | implementation "com.google.android.gms:play-services-mlkit-barcode-scanning:16.1.5" 88 | 89 | //region Local Unit Tests 90 | testImplementation "junit:junit:4.13.2" 91 | testImplementation "org.mockito:mockito-core:3.10.0" 92 | testImplementation "org.mockito.kotlin:mockito-kotlin:3.2.0" 93 | testImplementation "androidx.test:runner:1.3.0" 94 | testImplementation "androidx.arch.core:core-testing:2.1.0" 95 | //endregion 96 | 97 | //region Instrumented Tests 98 | androidTestImplementation "androidx.test.ext:junit:1.1.2" 99 | androidTestImplementation "androidx.test:runner:1.3.0" 100 | androidTestImplementation "org.mockito:mockito-android:3.10.0" 101 | androidTestImplementation "org.mockito.kotlin:mockito-kotlin:3.2.0" 102 | androidTestImplementation "androidx.arch.core:core-testing:2.1.0" 103 | // UI testing with Espresso 104 | androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0" 105 | androidTestImplementation "androidx.test:rules:1.3.0" 106 | //endregion 107 | } 108 | -------------------------------------------------------------------------------- /app/src/androidTest/java/uk/co/brightec/kbarcode/app/testutil/SingleFragmentActivity.kt: -------------------------------------------------------------------------------- 1 | package uk.co.brightec.kbarcode.app.testutil 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.widget.FrameLayout 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.fragment.app.Fragment 8 | 9 | /** 10 | * Used for testing fragments inside a fake activity. 11 | */ 12 | internal open class SingleFragmentActivity : AppCompatActivity() { 13 | 14 | private lateinit var container: FrameLayout 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | 19 | container = FrameLayout(this) 20 | container.id = View.generateViewId() 21 | setContentView(container) 22 | } 23 | 24 | fun setFragment(fragment: Fragment) { 25 | supportFragmentManager.beginTransaction() 26 | .add(container.id, fragment, "TEST") 27 | .commit() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/androidTest/java/uk/co/brightec/kbarcode/app/viewfinder/ViewfinderScreenTest.kt: -------------------------------------------------------------------------------- 1 | package uk.co.brightec.kbarcode.app.viewfinder 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.test.espresso.Espresso.onView 5 | import androidx.test.espresso.assertion.ViewAssertions.matches 6 | import androidx.test.espresso.matcher.RootMatchers.withDecorView 7 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 8 | import androidx.test.espresso.matcher.ViewMatchers.withId 9 | import androidx.test.espresso.matcher.ViewMatchers.withText 10 | import androidx.test.filters.LargeTest 11 | import androidx.test.rule.ActivityTestRule 12 | import androidx.test.rule.GrantPermissionRule 13 | import org.mockito.kotlin.doReturn 14 | import org.mockito.kotlin.mock 15 | import org.hamcrest.CoreMatchers.`is` 16 | import org.hamcrest.CoreMatchers.not 17 | import org.junit.Before 18 | import org.junit.Rule 19 | import org.junit.Test 20 | import uk.co.brightec.kbarcode.Barcode 21 | import uk.co.brightec.kbarcode.app.R 22 | import uk.co.brightec.kbarcode.app.testutil.SingleFragmentActivity 23 | 24 | @LargeTest 25 | internal class ViewfinderScreenTest { 26 | 27 | @Suppress("BooleanLiteralArgument") // Java method call 28 | @get:Rule 29 | val activityRule = ActivityTestRule(SingleFragmentActivity::class.java, true, true) 30 | 31 | @get:Rule 32 | val permissionRule: GrantPermissionRule = 33 | GrantPermissionRule.grant(android.Manifest.permission.CAMERA) 34 | 35 | private lateinit var barcode: MutableLiveData> 36 | private lateinit var viewModel: ViewfinderViewModel 37 | 38 | private lateinit var fragment: ViewfinderFragment 39 | 40 | @Before 41 | fun before() { 42 | barcode = MutableLiveData() 43 | viewModel = mock { 44 | on { this.barcode } doReturn barcode 45 | } 46 | 47 | fragment = ViewfinderFragment() 48 | fragment.viewModel = viewModel 49 | 50 | activityRule.activity.setFragment(fragment) 51 | } 52 | 53 | @Test 54 | fun nothing__barcodeLoading__showsProgress() { 55 | // GIVEN 56 | // nothing 57 | 58 | // WHEN 59 | barcode.postValue(Resource.Loading(mock())) 60 | 61 | // THEN 62 | onView(withId(R.id.progress)) 63 | .check(matches(isDisplayed())) 64 | } 65 | 66 | @Test 67 | fun nothing__barcodeError__showsToast() { 68 | // GIVEN 69 | // nothing 70 | 71 | // WHEN 72 | barcode.postValue(Resource.Error(mock(), mock())) 73 | 74 | // THEN 75 | onView(withText(R.string.error_processing_barcode)) 76 | .inRoot(isToast()) 77 | .check(matches(isDisplayed())) 78 | } 79 | 80 | @Test 81 | fun barcode__barcodeSuccess__showsToast() { 82 | // GIVEN 83 | val barcode = mock { 84 | on { displayValue } doReturn "123456" 85 | } 86 | 87 | // WHEN 88 | this.barcode.postValue(Resource.Success(barcode)) 89 | 90 | // THEN 91 | onView(withText(barcode.displayValue)) 92 | .inRoot(isToast()) 93 | .check(matches(isDisplayed())) 94 | } 95 | 96 | private fun isToast() = withDecorView(not(`is`(activityRule.activity.window.decorView))) 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 11 | 12 | 13 | 14 | 15 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/brightec/kbarcode/app/Application.kt: -------------------------------------------------------------------------------- 1 | package uk.co.brightec.kbarcode.app 2 | 3 | import android.app.Application 4 | import uk.co.brightec.kbarcode.KBarcode 5 | 6 | internal class Application : Application() { 7 | 8 | override fun onCreate() { 9 | super.onCreate() 10 | KBarcode.setDebugging(BuildConfig.DEBUG) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/brightec/kbarcode/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package uk.co.brightec.kbarcode.app 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import uk.co.brightec.kbarcode.app.camerax.CameraXActivity 6 | import uk.co.brightec.kbarcode.app.databinding.ActivityMainBinding 7 | import uk.co.brightec.kbarcode.app.viewfinder.ViewfinderActivity 8 | 9 | class MainActivity : AppCompatActivity() { 10 | 11 | private lateinit var binding: ActivityMainBinding 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | binding = ActivityMainBinding.inflate(layoutInflater) 16 | val view = binding.root 17 | setContentView(view) 18 | 19 | binding.buttonXml.setOnClickListener { 20 | val intent = XmlActivity.getStartingIntent(this) 21 | startActivity(intent) 22 | } 23 | binding.buttonXmlJava.setOnClickListener { 24 | val intent = XmlJavaActivity.getStartingIntent(this) 25 | startActivity(intent) 26 | } 27 | binding.buttonProgrammatic.setOnClickListener { 28 | val intent = ProgrammaticActivity.getStartingIntent(this) 29 | startActivity(intent) 30 | } 31 | binding.buttonViewfinder.setOnClickListener { 32 | val intent = ViewfinderActivity.getStartingIntent(this) 33 | startActivity(intent) 34 | } 35 | binding.buttonCamerax.setOnClickListener { 36 | val intent = CameraXActivity.getStartingIntent(this) 37 | startActivity(intent) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/brightec/kbarcode/app/ProgrammaticActivity.kt: -------------------------------------------------------------------------------- 1 | package uk.co.brightec.kbarcode.app 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.DialogInterface 6 | import android.content.Intent 7 | import android.content.pm.PackageManager 8 | import android.hardware.camera2.CameraCharacteristics 9 | import android.hardware.camera2.CameraMetadata 10 | import android.os.Bundle 11 | import androidx.appcompat.app.AlertDialog 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.core.app.ActivityCompat 14 | import androidx.core.content.ContextCompat 15 | import uk.co.brightec.kbarcode.Barcode 16 | import uk.co.brightec.kbarcode.BarcodeView 17 | import uk.co.brightec.kbarcode.Options 18 | import uk.co.brightec.kbarcode.app.databinding.ActivityProgrammaticBinding 19 | 20 | internal class ProgrammaticActivity : 21 | AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback { 22 | 23 | private lateinit var binding: ActivityProgrammaticBinding 24 | private lateinit var barcodeView: BarcodeView 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | binding = ActivityProgrammaticBinding.inflate(layoutInflater) 29 | val view = binding.root 30 | setContentView(view) 31 | window.decorView.setBackgroundColor(ContextCompat.getColor(this, R.color.black)) 32 | setTitle(R.string.title_programmatic) 33 | 34 | barcodeView = BarcodeView(this) 35 | barcodeView.setOptions( 36 | Options.Builder() 37 | .cameraFacing(CameraCharacteristics.LENS_FACING_BACK) 38 | .cameraFlashMode(CameraMetadata.FLASH_MODE_OFF) 39 | .barcodeFormats( 40 | intArrayOf( 41 | Barcode.FORMAT_CODABAR, Barcode.FORMAT_EAN_13, Barcode.FORMAT_EAN_8, 42 | Barcode.FORMAT_ITF, Barcode.FORMAT_UPC_A, Barcode.FORMAT_UPC_E 43 | ) 44 | ) 45 | .barcodesSort(null) 46 | .previewScaleType(BarcodeView.CENTER_INSIDE) 47 | .clearFocusDelay(BarcodeView.CLEAR_FOCUS_DELAY_DEFAULT) 48 | .build() 49 | ) 50 | binding.frameContainer.addView(barcodeView) 51 | 52 | lifecycle.addObserver(barcodeView) 53 | 54 | barcodeView.barcodes.observe(this) { barcodes -> 55 | val builder = StringBuilder() 56 | for (barcode in barcodes) { 57 | builder.append(barcode.displayValue).append("\n") 58 | } 59 | binding.textBarcodes.text = builder.toString() 60 | } 61 | 62 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) 63 | != PackageManager.PERMISSION_GRANTED 64 | ) { 65 | requestCameraPermission() 66 | } 67 | } 68 | 69 | override fun onRequestPermissionsResult( 70 | requestCode: Int, 71 | permissions: Array, 72 | grantResults: IntArray 73 | ) { 74 | when (requestCode) { 75 | REQUEST_PERMISSION_CAMERA -> if ( 76 | grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED 77 | ) { 78 | barcodeView.start() 79 | } 80 | else -> 81 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 82 | } 83 | } 84 | 85 | private fun requestCameraPermission() { 86 | if (ActivityCompat.shouldShowRequestPermissionRationale( 87 | this, 88 | Manifest.permission.CAMERA 89 | ) 90 | ) { 91 | AlertDialog.Builder(this) 92 | .setTitle(R.string.title_camera_rationale) 93 | .setMessage(R.string.message_camera_rationale) 94 | .setPositiveButton(R.string.action_ok) { _: DialogInterface, _: Int -> 95 | ActivityCompat.requestPermissions( 96 | this, 97 | arrayOf(Manifest.permission.CAMERA), 98 | REQUEST_PERMISSION_CAMERA 99 | ) 100 | } 101 | .show() 102 | } else { 103 | ActivityCompat.requestPermissions( 104 | this, 105 | arrayOf(Manifest.permission.CAMERA), 106 | REQUEST_PERMISSION_CAMERA 107 | ) 108 | } 109 | } 110 | 111 | companion object { 112 | 113 | private const val REQUEST_PERMISSION_CAMERA = 1 114 | 115 | fun getStartingIntent(context: Context) = Intent(context, ProgrammaticActivity::class.java) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/brightec/kbarcode/app/XmlActivity.kt: -------------------------------------------------------------------------------- 1 | package uk.co.brightec.kbarcode.app 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.DialogInterface 6 | import android.content.Intent 7 | import android.content.pm.PackageManager 8 | import android.os.Bundle 9 | import androidx.appcompat.app.AlertDialog 10 | import androidx.appcompat.app.AppCompatActivity 11 | import androidx.core.app.ActivityCompat 12 | import androidx.core.content.ContextCompat 13 | import uk.co.brightec.kbarcode.app.databinding.ActivityXmlBinding 14 | 15 | internal class XmlActivity : 16 | AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback { 17 | 18 | private lateinit var binding: ActivityXmlBinding 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | binding = ActivityXmlBinding.inflate(layoutInflater) 23 | val view = binding.root 24 | setContentView(view) 25 | window.decorView.setBackgroundColor(ContextCompat.getColor(this, R.color.black)) 26 | setTitle(R.string.title_xml) 27 | 28 | lifecycle.addObserver(binding.viewBarcode) 29 | 30 | binding.viewBarcode.barcodes.observe(this) { barcodes -> 31 | val builder = StringBuilder() 32 | for (barcode in barcodes) { 33 | builder.append(barcode.displayValue).append("\n") 34 | } 35 | binding.textBarcodes.text = builder.toString() 36 | } 37 | 38 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) 39 | != PackageManager.PERMISSION_GRANTED 40 | ) { 41 | requestCameraPermission() 42 | } 43 | } 44 | 45 | override fun onRequestPermissionsResult( 46 | requestCode: Int, 47 | permissions: Array, 48 | grantResults: IntArray 49 | ) { 50 | when (requestCode) { 51 | REQUEST_PERMISSION_CAMERA -> if ( 52 | grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED 53 | ) { 54 | binding.viewBarcode.start() 55 | } 56 | else -> 57 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 58 | } 59 | } 60 | 61 | private fun requestCameraPermission() { 62 | if (ActivityCompat.shouldShowRequestPermissionRationale( 63 | this, 64 | Manifest.permission.CAMERA 65 | ) 66 | ) { 67 | AlertDialog.Builder(this) 68 | .setTitle(R.string.title_camera_rationale) 69 | .setMessage(R.string.message_camera_rationale) 70 | .setPositiveButton(R.string.action_ok) { _: DialogInterface, _: Int -> 71 | ActivityCompat.requestPermissions( 72 | this, 73 | arrayOf(Manifest.permission.CAMERA), 74 | REQUEST_PERMISSION_CAMERA 75 | ) 76 | } 77 | .show() 78 | } else { 79 | ActivityCompat.requestPermissions( 80 | this, 81 | arrayOf(Manifest.permission.CAMERA), 82 | REQUEST_PERMISSION_CAMERA 83 | ) 84 | } 85 | } 86 | 87 | companion object { 88 | 89 | private const val REQUEST_PERMISSION_CAMERA = 1 90 | 91 | fun getStartingIntent(context: Context) = Intent(context, XmlActivity::class.java) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/brightec/kbarcode/app/XmlJavaActivity.java: -------------------------------------------------------------------------------- 1 | package uk.co.brightec.kbarcode.app; 2 | 3 | import android.Manifest; 4 | import android.content.Context; 5 | import android.content.DialogInterface; 6 | import android.content.Intent; 7 | import android.content.pm.PackageManager; 8 | import android.os.Bundle; 9 | import android.widget.TextView; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.Nullable; 13 | import androidx.appcompat.app.AlertDialog; 14 | import androidx.appcompat.app.AppCompatActivity; 15 | import androidx.core.app.ActivityCompat; 16 | import androidx.core.content.ContextCompat; 17 | import androidx.lifecycle.Observer; 18 | 19 | import java.util.List; 20 | 21 | import uk.co.brightec.kbarcode.Barcode; 22 | import uk.co.brightec.kbarcode.BarcodeView; 23 | 24 | public class XmlJavaActivity extends AppCompatActivity 25 | implements ActivityCompat.OnRequestPermissionsResultCallback { 26 | 27 | private static final int REQUEST_PERMISSION_CAMERA = 1; 28 | 29 | @NonNull 30 | public static Intent getStartingIntent(@NonNull Context context) { 31 | return new Intent(context, XmlJavaActivity.class); 32 | } 33 | 34 | private BarcodeView mBarcodeView; 35 | private TextView mTextBarcodes; 36 | 37 | @Override 38 | protected void onCreate(@Nullable Bundle savedInstanceState) { 39 | super.onCreate(savedInstanceState); 40 | setContentView(R.layout.activity_xml); 41 | getWindow().getDecorView().setBackgroundColor( 42 | ContextCompat.getColor(this, R.color.black) 43 | ); 44 | setTitle(R.string.title_xml_java); 45 | 46 | mBarcodeView = findViewById(R.id.view_barcode); 47 | mTextBarcodes = findViewById(R.id.text_barcodes); 48 | 49 | getLifecycle().addObserver(mBarcodeView); 50 | 51 | mBarcodeView.getBarcodes().observe(this, new Observer>() { 52 | @Override 53 | public void onChanged(@NonNull List barcodes) { 54 | StringBuilder builder = new StringBuilder(); 55 | for (Barcode barcode : barcodes) { 56 | builder.append(barcode.getDisplayValue()).append('\n'); 57 | } 58 | mTextBarcodes.setText(builder.toString()); 59 | } 60 | }); 61 | 62 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) 63 | != PackageManager.PERMISSION_GRANTED 64 | ) { 65 | requestCameraPermission(); 66 | } 67 | } 68 | 69 | @Override 70 | public void onRequestPermissionsResult( 71 | int requestCode, 72 | @NonNull String[] permissions, 73 | @NonNull int[] grantResults 74 | ) { 75 | switch (requestCode) { 76 | case REQUEST_PERMISSION_CAMERA: 77 | if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 78 | mBarcodeView.start(); 79 | } 80 | break; 81 | default: 82 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 83 | break; 84 | } 85 | } 86 | 87 | private void requestCameraPermission() { 88 | if (ActivityCompat.shouldShowRequestPermissionRationale( 89 | this, 90 | Manifest.permission.CAMERA 91 | )) { 92 | new AlertDialog.Builder(this) 93 | .setTitle(R.string.title_camera_rationale) 94 | .setMessage(R.string.message_camera_rationale) 95 | .setPositiveButton(R.string.action_ok, new DialogInterface.OnClickListener() { 96 | @Override 97 | public void onClick(DialogInterface dialog, int which) { 98 | ActivityCompat.requestPermissions( 99 | XmlJavaActivity.this, 100 | new String[]{Manifest.permission.CAMERA}, 101 | REQUEST_PERMISSION_CAMERA 102 | ); 103 | } 104 | }) 105 | .show(); 106 | } else { 107 | ActivityCompat.requestPermissions( 108 | this, 109 | new String[]{Manifest.permission.CAMERA}, 110 | REQUEST_PERMISSION_CAMERA 111 | ); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/brightec/kbarcode/app/camerax/BarcodeAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package uk.co.brightec.kbarcode.app.camerax 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.annotation.OptIn 6 | import androidx.camera.core.ExperimentalGetImage 7 | import androidx.camera.core.ImageAnalysis 8 | import androidx.camera.core.ImageProxy 9 | import androidx.core.content.ContextCompat 10 | import com.google.android.gms.tasks.Tasks 11 | import com.google.mlkit.vision.barcode.Barcode 12 | import com.google.mlkit.vision.barcode.BarcodeScanner 13 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions 14 | import com.google.mlkit.vision.barcode.BarcodeScanning 15 | import com.google.mlkit.vision.common.InputImage 16 | import java.util.concurrent.ExecutionException 17 | 18 | internal class BarcodeAnalyzer( 19 | context: Context, 20 | private val barcodeListener: (List) -> Unit, 21 | ) : ImageAnalysis.Analyzer { 22 | 23 | private val tag = BarcodeAnalyzer::class.simpleName ?: "BarcodeAnalyzer" 24 | private val mainExecutor = ContextCompat.getMainExecutor(context) 25 | private val scanner: BarcodeScanner = createScanner() 26 | 27 | @OptIn(ExperimentalGetImage::class) 28 | override fun analyze(imageProxy: ImageProxy) { 29 | val rotationDegrees = imageProxy.imageInfo.rotationDegrees 30 | val image = imageProxy.image 31 | if (image != null) { 32 | val inputImage = InputImage.fromMediaImage(image, rotationDegrees) 33 | val task = scanner.process(inputImage) 34 | try { 35 | val result = Tasks.await(task) 36 | if (result.isNotEmpty()) { 37 | mainExecutor.execute { 38 | barcodeListener.invoke(result) 39 | } 40 | } 41 | } catch (e: ExecutionException) { 42 | Log.e(tag, "Scanner process failed", e) 43 | } catch (e: InterruptedException) { 44 | Log.e(tag, "Scanner process interrupted", e) 45 | } 46 | } 47 | imageProxy.close() 48 | } 49 | 50 | private fun createScanner(): BarcodeScanner { 51 | val options = BarcodeScannerOptions.Builder() 52 | val formats = intArrayOf( 53 | Barcode.FORMAT_CODABAR, 54 | Barcode.FORMAT_EAN_13, 55 | Barcode.FORMAT_EAN_8, 56 | Barcode.FORMAT_ITF, 57 | Barcode.FORMAT_UPC_A, 58 | Barcode.FORMAT_UPC_E 59 | ) 60 | if (formats.size > 1) { 61 | @Suppress("SpreadOperator") // Required by Google API 62 | options.setBarcodeFormats( 63 | formats[0], *formats.slice(IntRange(1, formats.size - 1)).toIntArray() 64 | ) 65 | } else if (formats.size == 1) { 66 | options.setBarcodeFormats(formats[0]) 67 | } 68 | return BarcodeScanning.getClient(options.build()) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/brightec/kbarcode/app/camerax/CameraXActivity.kt: -------------------------------------------------------------------------------- 1 | package uk.co.brightec.kbarcode.app.camerax 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.DialogInterface 6 | import android.content.Intent 7 | import android.content.pm.PackageManager 8 | import android.os.Bundle 9 | import android.util.Log 10 | import android.view.MotionEvent 11 | import androidx.appcompat.app.AlertDialog 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.camera.core.Camera 14 | import androidx.camera.core.CameraControl 15 | import androidx.camera.core.CameraSelector 16 | import androidx.camera.core.FocusMeteringAction 17 | import androidx.camera.core.ImageAnalysis 18 | import androidx.camera.core.Preview 19 | import androidx.camera.lifecycle.ProcessCameraProvider 20 | import androidx.core.app.ActivityCompat 21 | import androidx.core.content.ContextCompat 22 | import uk.co.brightec.kbarcode.app.R 23 | import uk.co.brightec.kbarcode.app.databinding.ActivityCameraxBinding 24 | import java.util.concurrent.ExecutionException 25 | import java.util.concurrent.ExecutorService 26 | import java.util.concurrent.Executors 27 | 28 | internal class CameraXActivity : 29 | AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback { 30 | 31 | private val tag = CameraXActivity::class.simpleName ?: "CameraXActivity" 32 | 33 | private lateinit var binding: ActivityCameraxBinding 34 | private lateinit var cameraExecutor: ExecutorService 35 | private lateinit var camera: Camera 36 | 37 | override fun onCreate(savedInstanceState: Bundle?) { 38 | super.onCreate(savedInstanceState) 39 | binding = ActivityCameraxBinding.inflate(layoutInflater) 40 | val view = binding.root 41 | setContentView(view) 42 | setTitle(R.string.title_camerax) 43 | 44 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) 45 | != PackageManager.PERMISSION_GRANTED 46 | ) { 47 | requestCameraPermission() 48 | } else { 49 | startCamera() 50 | } 51 | 52 | cameraExecutor = Executors.newSingleThreadExecutor() 53 | } 54 | 55 | override fun onDestroy() { 56 | cameraExecutor.shutdown() 57 | super.onDestroy() 58 | } 59 | 60 | override fun onRequestPermissionsResult( 61 | requestCode: Int, 62 | permissions: Array, 63 | grantResults: IntArray 64 | ) { 65 | when (requestCode) { 66 | REQUEST_PERMISSION_CAMERA -> if ( 67 | grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED 68 | ) { 69 | startCamera() 70 | } 71 | else -> 72 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 73 | } 74 | } 75 | 76 | private fun requestCameraPermission() { 77 | if (ActivityCompat.shouldShowRequestPermissionRationale( 78 | this, 79 | Manifest.permission.CAMERA 80 | ) 81 | ) { 82 | AlertDialog.Builder(this) 83 | .setTitle(R.string.title_camera_rationale) 84 | .setMessage(R.string.message_camera_rationale) 85 | .setPositiveButton(R.string.action_ok) { _: DialogInterface, _: Int -> 86 | ActivityCompat.requestPermissions( 87 | this, 88 | arrayOf(Manifest.permission.CAMERA), 89 | REQUEST_PERMISSION_CAMERA 90 | ) 91 | } 92 | .show() 93 | } else { 94 | ActivityCompat.requestPermissions( 95 | this, 96 | arrayOf(Manifest.permission.CAMERA), 97 | REQUEST_PERMISSION_CAMERA 98 | ) 99 | } 100 | } 101 | 102 | private fun startCamera() { 103 | val cameraProviderFuture = ProcessCameraProvider.getInstance(this) 104 | cameraProviderFuture.addListener( 105 | { 106 | val previewUseCase = Preview.Builder() 107 | .build() 108 | .also { 109 | it.setSurfaceProvider(binding.preview.surfaceProvider) 110 | } 111 | val imageAnalyzerUseCase = ImageAnalysis.Builder() 112 | .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) 113 | .build() 114 | .also { 115 | it.setAnalyzer( 116 | cameraExecutor, 117 | BarcodeAnalyzer(this) { barcodes -> 118 | val builder = StringBuilder() 119 | for (barcode in barcodes) { 120 | builder.append(barcode.displayValue).append("\n") 121 | } 122 | binding.textBarcodes.text = builder.toString() 123 | } 124 | ) 125 | } 126 | val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() 127 | cameraProvider.unbindAll() 128 | val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA 129 | camera = cameraProvider.bindToLifecycle( 130 | this, cameraSelector, previewUseCase, imageAnalyzerUseCase 131 | ) 132 | setupTapToFocus() 133 | }, 134 | ContextCompat.getMainExecutor(this) 135 | ) 136 | } 137 | 138 | private fun setupTapToFocus() { 139 | binding.preview.setOnTouchListener { view, event -> 140 | val actionMasked = event.actionMasked 141 | if (actionMasked == MotionEvent.ACTION_UP) { 142 | view.performClick() 143 | return@setOnTouchListener false 144 | } 145 | if (actionMasked != MotionEvent.ACTION_DOWN) { 146 | return@setOnTouchListener false 147 | } 148 | 149 | val cameraControl = camera.cameraControl 150 | val factory = binding.preview.meteringPointFactory 151 | val point = factory.createPoint(event.x, event.y) 152 | val action = FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF) 153 | .addPoint(point, FocusMeteringAction.FLAG_AE) 154 | .addPoint(point, FocusMeteringAction.FLAG_AWB) 155 | .build() 156 | val future = cameraControl.startFocusAndMetering(action) 157 | future.addListener( 158 | { 159 | try { 160 | val result = future.get() 161 | Log.d(tag, "Focus Success: ${result.isFocusSuccessful}") 162 | } catch (e: ExecutionException) { 163 | if (e.cause is CameraControl.OperationCanceledException) { 164 | Log.d(tag, "Focus cancelled") 165 | } else { 166 | Log.e(tag, "Focus failed", e) 167 | } 168 | } catch (e: InterruptedException) { 169 | Log.e(tag, "Focus interrupted", e) 170 | } 171 | }, 172 | cameraExecutor 173 | ) 174 | true 175 | } 176 | } 177 | 178 | companion object { 179 | 180 | private const val REQUEST_PERMISSION_CAMERA = 1 181 | 182 | fun getStartingIntent(context: Context) = Intent(context, CameraXActivity::class.java) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/brightec/kbarcode/app/util/OpenForTesting.kt: -------------------------------------------------------------------------------- 1 | package uk.co.brightec.kbarcode.app.util 2 | 3 | /** 4 | * This annotation allows us to open some classes for mocking purposes while they are final in 5 | * release builds. 6 | */ 7 | @Target(AnnotationTarget.ANNOTATION_CLASS) 8 | internal annotation class OpenClass 9 | 10 | /** 11 | * Annotate a class with [OpenForTesting] if you want it to be extendable in debug builds. 12 | */ 13 | @OpenClass 14 | @Target(AnnotationTarget.CLASS) 15 | internal annotation class OpenForTesting 16 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/brightec/kbarcode/app/viewfinder/Resource.kt: -------------------------------------------------------------------------------- 1 | package uk.co.brightec.kbarcode.app.viewfinder 2 | 3 | /** 4 | * A generic class that describes some data with a status 5 | * Inspired by: 6 | * https://developer.android.com/topic/libraries/architecture/guide.html#addendum 7 | * https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample 8 | */ 9 | @Suppress("unused") 10 | sealed class Resource { 11 | 12 | data class Loading(val data: T?) : Resource() 13 | 14 | data class Error(val error: Exception, val data: T?) : Resource() 15 | 16 | data class Success(val data: T) : Resource() 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/brightec/kbarcode/app/viewfinder/ViewfinderActivity.kt: -------------------------------------------------------------------------------- 1 | package uk.co.brightec.kbarcode.app.viewfinder 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.core.content.ContextCompat 8 | import androidx.fragment.app.Fragment 9 | import uk.co.brightec.kbarcode.app.R 10 | 11 | internal class ViewfinderActivity : AppCompatActivity() { 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | setContentView(R.layout.activity_viewfinder) 16 | window.decorView.setBackgroundColor(ContextCompat.getColor(this, R.color.black)) 17 | 18 | var fragment = supportFragmentManager.findFragmentById(R.id.frame_container) 19 | if (fragment == null) { 20 | fragment = ViewfinderFragment() 21 | setFragment(fragment) 22 | } 23 | } 24 | 25 | private fun setFragment(fragment: Fragment) { 26 | supportFragmentManager.beginTransaction() 27 | .add(R.id.frame_container, fragment, "TEST") 28 | .commit() 29 | } 30 | 31 | companion object { 32 | 33 | fun getStartingIntent(context: Context) = Intent(context, ViewfinderActivity::class.java) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/brightec/kbarcode/app/viewfinder/ViewfinderFragment.kt: -------------------------------------------------------------------------------- 1 | package uk.co.brightec.kbarcode.app.viewfinder 2 | 3 | import android.Manifest 4 | import android.content.DialogInterface 5 | import android.content.pm.PackageManager 6 | import android.os.Bundle 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.Toast 11 | import androidx.annotation.VisibleForTesting 12 | import androidx.appcompat.app.AlertDialog 13 | import androidx.core.app.ActivityCompat 14 | import androidx.core.content.ContextCompat 15 | import androidx.fragment.app.Fragment 16 | import androidx.lifecycle.ViewModelProvider 17 | import uk.co.brightec.kbarcode.app.R 18 | import uk.co.brightec.kbarcode.app.databinding.FragmentViewfinderBinding 19 | 20 | internal class ViewfinderFragment : Fragment() { 21 | 22 | private var _binding: FragmentViewfinderBinding? = null 23 | 24 | // This property is only valid between onCreateView and onDestroyView. 25 | private val binding get() = _binding!! 26 | 27 | @VisibleForTesting 28 | lateinit var viewModel: ViewfinderViewModel 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | // Set the viewModel if it hasn't already 33 | // It could be set already in a test 34 | if (!::viewModel.isInitialized) { 35 | viewModel = ViewModelProvider(this).get(ViewfinderViewModel::class.java) 36 | } 37 | } 38 | 39 | override fun onCreateView( 40 | inflater: LayoutInflater, 41 | container: ViewGroup?, 42 | savedInstanceState: Bundle? 43 | ): View { 44 | _binding = FragmentViewfinderBinding.inflate(inflater, container, false) 45 | return binding.root 46 | } 47 | 48 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 49 | requireActivity().setTitle(R.string.title_viewfinder) 50 | 51 | lifecycle.addObserver(binding.viewBarcode) 52 | 53 | viewModel.setData(binding.viewBarcode.barcode) 54 | viewModel.barcode.observe(viewLifecycleOwner) { resource -> 55 | when (resource) { 56 | is Resource.Success -> { 57 | binding.progress.visibility = View.GONE 58 | val barcode = resource.data 59 | Toast.makeText( 60 | requireContext(), barcode.displayValue, Toast.LENGTH_SHORT 61 | ).show() 62 | binding.viewBarcode.resume() 63 | } 64 | is Resource.Error -> { 65 | binding.progress.visibility = View.GONE 66 | Toast.makeText( 67 | requireContext(), 68 | R.string.error_processing_barcode, Toast.LENGTH_SHORT 69 | ).show() 70 | binding.viewBarcode.resume() 71 | } 72 | is Resource.Loading -> { 73 | binding.viewBarcode.pause() 74 | binding.progress.visibility = View.VISIBLE 75 | } 76 | } 77 | } 78 | 79 | if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) 80 | != PackageManager.PERMISSION_GRANTED 81 | ) { 82 | requestCameraPermission() 83 | } 84 | } 85 | 86 | override fun onDestroyView() { 87 | _binding = null 88 | super.onDestroyView() 89 | } 90 | 91 | override fun onRequestPermissionsResult( 92 | requestCode: Int, 93 | permissions: Array, 94 | grantResults: IntArray 95 | ) { 96 | when (requestCode) { 97 | REQUEST_PERMISSION_CAMERA -> if ( 98 | grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED 99 | ) { 100 | binding.viewBarcode.start() 101 | } 102 | else -> 103 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 104 | } 105 | } 106 | 107 | private fun requestCameraPermission() { 108 | if (ActivityCompat.shouldShowRequestPermissionRationale( 109 | requireActivity(), 110 | Manifest.permission.CAMERA 111 | ) 112 | ) { 113 | AlertDialog.Builder(requireContext()) 114 | .setTitle(R.string.title_camera_rationale) 115 | .setMessage(R.string.message_camera_rationale) 116 | .setPositiveButton(R.string.action_ok) { _: DialogInterface, _: Int -> 117 | requestPermissions( 118 | arrayOf(Manifest.permission.CAMERA), 119 | REQUEST_PERMISSION_CAMERA 120 | ) 121 | } 122 | .show() 123 | } else { 124 | requestPermissions( 125 | arrayOf(Manifest.permission.CAMERA), 126 | REQUEST_PERMISSION_CAMERA 127 | ) 128 | } 129 | } 130 | 131 | companion object { 132 | 133 | private const val REQUEST_PERMISSION_CAMERA = 1 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/brightec/kbarcode/app/viewfinder/ViewfinderViewModel.kt: -------------------------------------------------------------------------------- 1 | package uk.co.brightec.kbarcode.app.viewfinder 2 | 3 | import android.app.Application 4 | import android.os.Handler 5 | import android.os.Looper 6 | import androidx.lifecycle.AndroidViewModel 7 | import androidx.lifecycle.LiveData 8 | import androidx.lifecycle.MediatorLiveData 9 | import uk.co.brightec.kbarcode.Barcode 10 | import uk.co.brightec.kbarcode.app.util.OpenForTesting 11 | 12 | @OpenForTesting 13 | internal class ViewfinderViewModel( 14 | application: Application 15 | ) : AndroidViewModel(application) { 16 | 17 | private val _barcode = MediatorLiveData>() 18 | val barcode: LiveData> 19 | get() = _barcode 20 | 21 | fun setData(data: LiveData) { 22 | _barcode.addSource(data) { barcode -> 23 | _barcode.value = Resource.Loading(barcode) 24 | Handler(Looper.getMainLooper()).postDelayed( 25 | { 26 | _barcode.value = Resource.Success(barcode) 27 | }, 28 | DELAY 29 | ) 30 | } 31 | } 32 | 33 | companion object { 34 | 35 | private const val DELAY = 5000L 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/img_viewfinder_black_125dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_camerax.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 |