├── .editorconfig ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── renovate.json └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── detekt.yml ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── quickie ├── build.gradle.kts └── src │ ├── bundled │ └── kotlin │ │ └── io │ │ └── github │ │ └── g00fy2 │ │ └── quickie │ │ └── utils │ │ └── MlKitErrorHandler.kt │ ├── main │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── io │ │ │ └── github │ │ │ └── g00fy2 │ │ │ └── quickie │ │ │ ├── QRCodeAnalyzer.kt │ │ │ ├── QROverlayView.kt │ │ │ ├── QRResult.kt │ │ │ ├── QRScannerActivity.kt │ │ │ ├── ScanCustomCode.kt │ │ │ ├── ScanQRCode.kt │ │ │ ├── config │ │ │ ├── BarcodeFormat.kt │ │ │ ├── ParcelableScannerConfig.kt │ │ │ └── ScannerConfig.kt │ │ │ ├── content │ │ │ ├── ParcelableContent.kt │ │ │ └── QRContent.kt │ │ │ └── extensions │ │ │ ├── BarcodeExtensions.kt │ │ │ ├── IntentExtensions.kt │ │ │ └── ScannerConfigExtensions.kt │ └── res │ │ ├── drawable │ │ ├── quickie_bg_round.xml │ │ ├── quickie_ic_close.xml │ │ ├── quickie_ic_qrcode.xml │ │ └── quickie_ic_torch.xml │ │ ├── layout │ │ ├── quickie_overlay_view.xml │ │ └── quickie_scanner_activity.xml │ │ ├── values-af │ │ └── strings.xml │ │ ├── values-am │ │ └── strings.xml │ │ ├── values-ar │ │ └── strings.xml │ │ ├── values-as │ │ └── strings.xml │ │ ├── values-az │ │ └── strings.xml │ │ ├── values-be │ │ └── strings.xml │ │ ├── values-bg │ │ └── strings.xml │ │ ├── values-bn │ │ └── strings.xml │ │ ├── values-bs │ │ └── strings.xml │ │ ├── values-ca │ │ └── strings.xml │ │ ├── values-cs │ │ └── strings.xml │ │ ├── values-da │ │ └── strings.xml │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-el │ │ └── strings.xml │ │ ├── values-en │ │ └── strings.xml │ │ ├── values-es-rUS │ │ └── strings.xml │ │ ├── values-es │ │ └── strings.xml │ │ ├── values-et │ │ └── strings.xml │ │ ├── values-eu │ │ └── strings.xml │ │ ├── values-fa │ │ └── strings.xml │ │ ├── values-fi │ │ └── strings.xml │ │ ├── values-fr-rCA │ │ └── strings.xml │ │ ├── values-fr │ │ └── strings.xml │ │ ├── values-gl │ │ └── strings.xml │ │ ├── values-gu │ │ └── strings.xml │ │ ├── values-hi │ │ └── strings.xml │ │ ├── values-hr │ │ └── strings.xml │ │ ├── values-hu │ │ └── strings.xml │ │ ├── values-hy │ │ └── strings.xml │ │ ├── values-in │ │ └── strings.xml │ │ ├── values-is │ │ └── strings.xml │ │ ├── values-it │ │ └── strings.xml │ │ ├── values-iw │ │ └── strings.xml │ │ ├── values-ja │ │ └── strings.xml │ │ ├── values-ka │ │ └── strings.xml │ │ ├── values-kk │ │ └── strings.xml │ │ ├── values-km │ │ └── strings.xml │ │ ├── values-kn │ │ └── strings.xml │ │ ├── values-ko │ │ └── strings.xml │ │ ├── values-ky │ │ └── strings.xml │ │ ├── values-lo │ │ └── strings.xml │ │ ├── values-lt │ │ └── strings.xml │ │ ├── values-lv │ │ └── strings.xml │ │ ├── values-mk │ │ └── strings.xml │ │ ├── values-ml │ │ └── strings.xml │ │ ├── values-mn │ │ └── strings.xml │ │ ├── values-mr │ │ └── strings.xml │ │ ├── values-ms │ │ └── strings.xml │ │ ├── values-my │ │ └── strings.xml │ │ ├── values-nb │ │ └── strings.xml │ │ ├── values-ne │ │ └── strings.xml │ │ ├── values-nl │ │ └── strings.xml │ │ ├── values-or │ │ └── strings.xml │ │ ├── values-pa │ │ └── strings.xml │ │ ├── values-pl │ │ └── strings.xml │ │ ├── values-pt │ │ └── strings.xml │ │ ├── values-ro │ │ └── strings.xml │ │ ├── values-ru │ │ └── strings.xml │ │ ├── values-si │ │ └── strings.xml │ │ ├── values-sk │ │ └── strings.xml │ │ ├── values-sl │ │ └── strings.xml │ │ ├── values-sq │ │ └── strings.xml │ │ ├── values-sr │ │ └── strings.xml │ │ ├── values-sv │ │ └── strings.xml │ │ ├── values-sw │ │ └── strings.xml │ │ ├── values-ta │ │ └── strings.xml │ │ ├── values-te │ │ └── strings.xml │ │ ├── values-th │ │ └── strings.xml │ │ ├── values-tl │ │ └── strings.xml │ │ ├── values-tr │ │ └── strings.xml │ │ ├── values-uk │ │ └── strings.xml │ │ ├── values-ur │ │ └── strings.xml │ │ ├── values-uz │ │ └── strings.xml │ │ ├── values-v23 │ │ └── themes.xml │ │ ├── values-v27 │ │ └── themes.xml │ │ ├── values-v29 │ │ └── themes.xml │ │ ├── values-vi │ │ └── strings.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values-zh-rHK │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ └── strings.xml │ │ ├── values-zu │ │ └── strings.xml │ │ └── values │ │ ├── colors.xml │ │ ├── public.xml │ │ ├── strings.xml │ │ └── themes.xml │ ├── test │ └── kotlin │ │ └── io │ │ └── github │ │ └── g00fy2 │ │ └── quickie │ │ └── BarcodeFormatsTest.kt │ └── unbundled │ ├── AndroidManifest.xml │ └── kotlin │ └── io │ └── github │ └── g00fy2 │ └── quickie │ └── utils │ └── MlKitErrorHandler.kt ├── sample ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── kotlin │ └── io │ │ └── github │ │ └── g00fy2 │ │ └── quickiesample │ │ ├── MainActivity.kt │ │ ├── SampleApp.kt │ │ └── quicksettingstile │ │ └── QuickieTileService.kt │ └── res │ ├── drawable │ ├── ic_launcher_foreground.xml │ ├── ic_qs_qrcode.xml │ └── ic_scan_barcode.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ └── ic_launcher.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── raw │ └── keep.xml │ └── values │ ├── colors.xml │ └── strings.xml └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | charset=utf-8 5 | end_of_line=lf 6 | insert_final_newline=false 7 | 8 | max_line_length=120 9 | 10 | indent_size=2 11 | indent_style=space 12 | 13 | [*.{kt,kts}] 14 | ij_kotlin_name_count_to_use_star_import=2147483647 15 | ij_kotlin_name_count_to_use_star_import_for_members=2147483647 16 | ij_kotlin_imports_layout=*,java.**,javax.**,kotlin.**,^ 17 | ij_kotlin_line_break_after_multiline_when_entry=false 18 | 19 | [*.xml] 20 | ij_continuation_indent_size=4 21 | ij_xml_keep_line_breaks=true 22 | ij_xml_space_inside_empty_tag=false -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | twirth.development@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to quickie 2 | 3 | Welcome and thanks for being interested in contributing to quickie! 4 | 5 | Quickie is a minimal barcode scanning library with an opinionated design. 6 | Therefore there are currently no plans to allow enhanced customization of the scanner overview ui. 7 | See this comment for further explanation [#14](https://github.com/G00fY2/quickie/pull/14#issuecomment-877804346). 8 | 9 | ## Creating Pull Requests 10 | * This project uses Git Flow as branching strategy. Make sure to use the **develop** branch as the base branch when creating a pull request. 11 | * For first time contributors GitHub will not run the GitHub Actions automatically. You should make sure that all tasks succeed before committing (see [Code Contributions](#code-contributions)). 12 | 13 | ## Code Contributions 14 | Make sure to get working code on a personal branch with tests and sanity checks passing before you submit a PR: 15 | ```shell 16 | ./gradlew detektBundledDebug detektUnbundledDebug 17 | ./gradlew test 18 | ./gradlew :sample:lintBundledDebug 19 | ./gradlew :sample:lintUnbundledDebug 20 | ./gradlew :sample:assembleBundledDebug 21 | ./gradlew :sample:assembleUnbundledDebug 22 | ``` 23 | Please make every effort to follow existing conventions and style in order to keep the code as readable as possible. 24 | 25 | Contribute code changes through GitHub by forking the repository and sending a pull request. I will squash all pull requests on merge. 26 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":disableDependencyDashboard" 5 | ], 6 | "baseBranches": [ 7 | "develop" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | - main 8 | pull_request: 9 | branches: 10 | - develop 11 | - main 12 | 13 | env: 14 | JAVA_DISTRO: 'zulu' 15 | JAVA_VERSION: '21' 16 | 17 | jobs: 18 | detekt: 19 | name: Run detekt 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-java@v4 25 | with: 26 | distribution: ${{ env.JAVA_DISTRO }} 27 | java-version: ${{ env.JAVA_VERSION }} 28 | - name: Run detekt with ktlint 29 | run: ./gradlew detektBundledDebug detektUnbundledDebug 30 | 31 | unit_tests: 32 | name: Run unit tests 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: actions/setup-java@v4 38 | with: 39 | distribution: ${{ env.JAVA_DISTRO }} 40 | java-version: ${{ env.JAVA_VERSION }} 41 | - name: Run bundled and unbundled unit tests 42 | run: ./gradlew test 43 | 44 | android_lint: 45 | name: Android lint 46 | runs-on: ubuntu-latest 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: actions/setup-java@v4 51 | with: 52 | distribution: ${{ env.JAVA_DISTRO }} 53 | java-version: ${{ env.JAVA_VERSION }} 54 | - name: Run Android lint 55 | run: ./gradlew :sample:lintBundledDebug :sample:lintUnbundledDebug 56 | 57 | build_bundled: 58 | name: Build bundled debug 59 | runs-on: ubuntu-latest 60 | 61 | steps: 62 | - uses: actions/checkout@v4 63 | - uses: actions/setup-java@v4 64 | with: 65 | distribution: ${{ env.JAVA_DISTRO }} 66 | java-version: ${{ env.JAVA_VERSION }} 67 | - name: Build debug bundled sample app 68 | run: ./gradlew :sample:assembleBundledDebug 69 | 70 | build_unbundled: 71 | name: Build unbundled debug 72 | runs-on: ubuntu-latest 73 | 74 | steps: 75 | - uses: actions/checkout@v4 76 | - uses: actions/setup-java@v4 77 | with: 78 | distribution: ${{ env.JAVA_DISTRO }} 79 | java-version: ${{ env.JAVA_VERSION }} 80 | - name: Build debug unbundled sample app 81 | run: ./gradlew :sample:assembleUnbundledDebug 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Reference gitignore https://gist.github.com/G00fY2/81c0aa67943cca021e915840bf73ee1b 2 | 3 | # Built application files 4 | *.apk 5 | *.ap_ 6 | *.aab 7 | 8 | # Files for the ART/Dalvik VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # Generated files 15 | bin/ 16 | gen/ 17 | out/ 18 | 19 | # Gradle files 20 | .gradle 21 | .gradle/ 22 | build/ 23 | gradle-app.setting 24 | .gradletasknamecache 25 | 26 | # Signing files 27 | .signing/ 28 | 29 | # Local configuration file (sdk path, etc) 30 | local.properties 31 | 32 | # Proguard folder generated by Eclipse 33 | proguard/ 34 | 35 | # Log Files 36 | *.log 37 | 38 | # Android Studio 39 | /*/bundled/ 40 | /*/unbundled/ 41 | /*/build/ 42 | /*/local.properties 43 | /*/out 44 | /*/*/build 45 | /*/*/production 46 | /*/*/staging 47 | *.ipr 48 | *~ 49 | *.swp 50 | 51 | # Android Patch 52 | gen-external-apklibs 53 | 54 | # Android Studio Navigation editor temp files 55 | .navigation/ 56 | 57 | # Android Studio captures folder 58 | captures/ 59 | 60 | # IntelliJ 61 | *.iml 62 | *.iws 63 | .idea/* 64 | 65 | # Keystore files 66 | *.jks 67 | *.keystore 68 | 69 | # External native build folder generated in Android Studio 2.2 and later 70 | .externalNativeBuild 71 | 72 | # NDK 73 | obj/ 74 | 75 | # CMake 76 | cmake-build-*/ 77 | 78 | # Google Services (e.g. APIs or Firebase) 79 | google-services.json 80 | 81 | # Freeline 82 | freeline.py 83 | freeline/ 84 | freeline_project_description.json 85 | 86 | # fastlane 87 | fastlane/report.xml 88 | fastlane/Preview.html 89 | fastlane/screenshots 90 | fastlane/test_output 91 | fastlane/readme.md 92 | 93 | #Crashlytics 94 | crashlytics-build.properties 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | fabric.properties 98 | 99 | ## Plugin-specific files: 100 | 101 | # mpeltonen/sbt-idea plugin 102 | .idea_modules/ 103 | 104 | # Mongo Explorer plugin 105 | .idea/mongoSettings.xml 106 | 107 | # JIRA plugin 108 | atlassian-ide-plugin.xml 109 | 110 | # Cursive Clojure plugin 111 | .idea/replstate.xml 112 | 113 | # Editor-based Rest Client 114 | .idea/httpRequests 115 | 116 | ## OS specific files: 117 | 118 | # General 119 | .DS_Store 120 | .DS_Store? 121 | .AppleDouble 122 | .LSOverride 123 | 124 | # Icon must end with two \r 125 | Icon 126 | 127 | 128 | # Thumbnails 129 | ._* 130 | 131 | # Files that might appear in the root of a volume 132 | .DocumentRevisions-V100 133 | .fseventsd 134 | .Spotlight-V100 135 | .TemporaryItems 136 | .Trashes 137 | .VolumeIcon.icns 138 | .com.apple.timemachine.donotpresent 139 | 140 | # Directories potentially created on remote AFP share 141 | .AppleDB 142 | .AppleDesktop 143 | Network Trash Folder 144 | Temporary Items 145 | .apdisk 146 | 147 | # Windows thumbnail cache files 148 | Thumbs.db 149 | ehthumbs.db 150 | ehthumbs_vista.db 151 | 152 | # Dump file 153 | *.stackdump 154 | 155 | # Folder config file 156 | [Dd]esktop.ini 157 | 158 | # Recycle Bin used on file shares 159 | $RECYCLE.BIN/ 160 | 161 | # Windows Installer files 162 | *.cab 163 | *.msi 164 | *.msix 165 | *.msm 166 | *.msp 167 | 168 | # Windows shortcuts 169 | *.lnk 170 | 171 | # temporary files which can be created if a process still has a handle open of a deleted file 172 | .fuse_hidden* 173 | 174 | # KDE directory preferences 175 | .directory 176 | 177 | # Linux trash folder which might appear on any partition or disk 178 | .Trash-* 179 | 180 | # .nfs files are created when an open file is removed but is still being accessed 181 | .nfs* 182 | 183 | # Directory for Kotlin data in Gradle projects 184 | .kotlin/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2021 Thomas Wirth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | associated documentation files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial 12 | portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 15 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![quickie logo](https://raw.githubusercontent.com/G00fY2/quickie/gh-pages/media/logo_dark.svg#gh-dark-mode-only) 2 | ![quickie logo](https://raw.githubusercontent.com/G00fY2/quickie/gh-pages/media/logo_light.svg#gh-light-mode-only) 3 | 4 | **quickie** is a Quick Response (QR) Code scanning library for Android that is based on CameraX and ML Kit on-device barcode detection. It's an alternative to ZXing based libraries and written in Kotlin. **quickie** features: 5 | - Easy API for launching the QR scanner and receiving results by using the new Activity Result API 6 | - Modern design, edge-to-edge scanning view with multilingual user hint 7 | - Android Jetpack CameraX for communicating with the camera and showing the preview 8 | - ML Kit Vision API for best, fully on-device barcode recognition and decoding 9 | 10 | ## Download [![Maven Central](https://img.shields.io/maven-central/v/io.github.g00fy2.quickie/quickie-unbundled)](https://search.maven.org/search?q=g:io.github.g00fy2.quickie) 11 | There are two different flavors available on `mavenCentral()`: 12 | 13 | | Bundled | Unbundled | 14 | |-------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------| 15 | | ML Kit model is bundled inside app (independent of Google Services) | ML Kit model will be automatically downloaded via Play Services (once while installing/updating the app) | 16 | | About 2.5 MB app size increase per ABI (you should use App Bundle or ABI splitting) | About 550 KB app size increase | 17 | | V3 model is used (faster, more accurate) | Currently V1 model will be downloaded | 18 | 19 | ```kotlin 20 | // bundled: 21 | implementation("io.github.g00fy2.quickie:quickie-bundled:1.11.0") 22 | 23 | // unbundled: 24 | implementation("io.github.g00fy2.quickie:quickie-unbundled:1.11.0") 25 | ``` 26 | 27 | ## Quick Start 28 | 29 | #### View-based 30 | To use the QR scanner simply register the `ScanQRCode()` ActivityResultContract together with a callback during `init` or `onCreate()` lifecycle of your Activity/Fragment and use the returned ActivityResultLauncher to launch the QR scanner Activity. 31 | ```kotlin 32 | val scanQrCodeLauncher = registerForActivityResult(ScanQRCode()) { result -> 33 | // handle QRResult 34 | } 35 | 36 | override fun onCreate(savedInstanceState: Bundle?) { 37 | super.onCreate(savedInstanceState) 38 | … 39 | binding.button.setOnClickListener { scanQrCodeLauncher.launch(null) } 40 | } 41 | ``` 42 | 43 | Check out the [sample](https://github.com/G00fY2/quickie/tree/develop/sample) inside this repo or visit the official [Activity Result API documentation](https://developer.android.com/training/basics/intents/result) for more information. 44 | 45 | #### Jetpack Compose 46 | Use the `rememberLauncherForActivityResult()` API to register the `ScanQRCode()` ActivityResultContract together with a callback in your composable: 47 | ```kotlin 48 | @Composable 49 | fun GetQRCodeExample() { 50 | val scanQrCodeLauncher = rememberLauncherForActivityResult(ScanQRCode()) { result -> 51 | // handle QRResult 52 | } 53 | 54 | Button(onClick = { scanQrCodeLauncher.launch(null) }) { 55 | … 56 | } 57 | ``` 58 | Check out the official [Compose Activity Result documentation](https://developer.android.com/jetpack/compose/libraries#activity_result) for more information. 59 | 60 | ### Responses 61 | The activity result is a subclass of the sealed `QRResult` class: 62 | 63 | 1. `QRSuccess` when ML Kit successfully detected a QR code 64 | * wraps a `QRContent` object 65 | 2. `QRUserCanceled` when the Activity got canceled by the user 66 | 3. `QRMissingPermission` when the user didn't accept the camera permission 67 | 4. `QRError` when CameraX or ML Kit threw an exception 68 | * wraps the `exception` 69 | 70 | #### Content 71 | The content type of the QR code detected by ML Kit is wrapped inside a subclass of the sealed `QRContent` class which always provides a `rawBytes` and `rawValue` (will only be `null` for non-UTF8 barcodes). 72 | 73 | Currently, supported subtypes are: 74 | `Plain`, `Wifi`, `Url`, `Sms`, `GeoPoint`, `Email`, `Phone`, `ContactInfo`, `CalendarEvent` 75 | 76 | See the ML Kit [Barcode documentation](https://developers.google.com/android/reference/com/google/mlkit/vision/barcode/common/Barcode#nested-class-summary) for further details. 77 | 78 | ### Customization 79 | Use the `ScanCustomCode()` ActivityResultContract to create a configurable barcode scan. When launching the ActivityResultLauncher pass in a `ScannerConfig` object: 80 | 81 |
82 | BarcodeFormat options 83 | 84 | ```kotlin 85 | BarcodeFormat.FORMAT_ALL_FORMATS 86 | BarcodeFormat.FORMAT_CODE_128 87 | BarcodeFormat.FORMAT_CODE_39 88 | BarcodeFormat.FORMAT_CODE_93 89 | BarcodeFormat.FORMAT_CODABAR 90 | BarcodeFormat.FORMAT_DATA_MATRIX 91 | BarcodeFormat.FORMAT_EAN_13 92 | BarcodeFormat.FORMAT_EAN_8 93 | BarcodeFormat.FORMAT_ITF 94 | BarcodeFormat.FORMAT_QR_CODE 95 | BarcodeFormat.FORMAT_UPC_A 96 | BarcodeFormat.FORMAT_UPC_E 97 | BarcodeFormat.FORMAT_PDF417 98 | BarcodeFormat.FORMAT_AZTEC 99 | ``` 100 |
101 | 102 | ```kotlin 103 | val scanCustomCode = registerForActivityResult(ScanCustomCode(), ::handleResult) 104 | 105 | override fun onCreate(savedInstanceState: Bundle?) { 106 | super.onCreate(savedInstanceState) 107 | … 108 | binding.button.setOnClickListener { 109 | scanCustomCode.launch( 110 | ScannerConfig.build { 111 | setBarcodeFormats(listOf(BarcodeFormat.FORMAT_CODE_128)) // set interested barcode formats 112 | setOverlayStringRes(R.string.scan_barcode) // string resource used for the scanner overlay 113 | setOverlayDrawableRes(R.drawable.ic_scan_barcode) // drawable resource used for the scanner overlay 114 | setHapticSuccessFeedback(false) // enable (default) or disable haptic feedback when a barcode was detected 115 | setShowTorchToggle(true) // show or hide (default) torch/flashlight toggle button 116 | setShowCloseButton(true) // show or hide (default) close button 117 | setHorizontalFrameRatio(2.2f) // set the horizontal overlay ratio (default is 1 / square frame) 118 | setUseFrontCamera(true) // use the front camera 119 | setKeepScreenOn(true) // keep the device's screen turned on 120 | } 121 | ) 122 | } 123 | } 124 | 125 | fun handleResult(result: QRResult) { 126 | … 127 | ``` 128 | > [!TIP] 129 | > You can optionally [pass in an ActivityOptionsCompat object](https://developer.android.com/reference/androidx/activity/result/ActivityResultLauncher#launch(I,%20androidx.core.app.ActivityOptionsCompat)) when launching the ActivityResultLauncher to control the scanner launch animation. 130 | 131 | ## Screenshots / Sample App 132 | You can find the sample app APKs inside the [release](https://github.com/G00fY2/quickie/releases) assets. 133 | 134 | ![Image](https://raw.githubusercontent.com/G00fY2/quickie/gh-pages/media/quickie-device-demo.png) 135 | 136 | ## Requirements 137 | * AndroidX 138 | * Min SDK 21+ (required by CameraX) 139 | * (Google Play Services available on the end device if using `quickie-unbundled`) 140 | 141 | ## Terms & Privacy 142 | See [ML Kit Terms & Privacy](https://developers.google.com/ml-kit/terms) 143 | 144 | ## Contributing 145 | See [CONTRIBUTING](.github/CONTRIBUTING.md) 146 | 147 | Thanks to everyone who contributed to quickie! 148 | 149 | ## License 150 | The MIT License (MIT) 151 | 152 | Copyright (C) 2022 Thomas Wirth 153 | 154 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 155 | associated documentation files (the "Software"), to deal in the Software without restriction, 156 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 157 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 158 | subject to the following conditions: 159 | 160 | The above copyright notice and this permission notice shall be included in all copies or substantial 161 | portions of the Software. 162 | 163 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 164 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 165 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 166 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 167 | OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 168 | 169 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.android.build.gradle.BaseExtension 2 | import com.android.build.gradle.BasePlugin 3 | import io.gitlab.arturbosch.detekt.Detekt 4 | import io.gitlab.arturbosch.detekt.extensions.DetektExtension 5 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 6 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 7 | 8 | plugins { 9 | alias(libs.plugins.android.application) apply false 10 | alias(libs.plugins.android.library) apply false 11 | alias(libs.plugins.kotlin.android) apply false 12 | alias(libs.plugins.kotlin.parcelize) apply false 13 | alias(libs.plugins.kotlin.dokka) apply false 14 | alias(libs.plugins.detekt) apply false 15 | } 16 | 17 | subprojects { 18 | tasks.withType().configureEach { 19 | compilerOptions { 20 | allWarningsAsErrors = true 21 | progressiveMode = true 22 | jvmTarget = JvmTarget.JVM_11 23 | if (this@subprojects.name != "sample") { 24 | freeCompilerArgs.add("-Xexplicit-api=strict") 25 | } 26 | } 27 | } 28 | 29 | plugins.withType().configureEach { 30 | extensions.configure { 31 | compileSdkVersion(libs.versions.androidconfig.compileSdk.get().toInt()) 32 | buildToolsVersion(libs.versions.androidconfig.buildTools.get()) 33 | defaultConfig { 34 | minSdk = libs.versions.androidconfig.minSdk.get().toInt() 35 | targetSdk = libs.versions.androidconfig.targetSdk.get().toInt() 36 | } 37 | compileOptions { 38 | sourceCompatibility = JavaVersion.VERSION_11 39 | targetCompatibility = JavaVersion.VERSION_11 40 | } 41 | } 42 | } 43 | 44 | apply(plugin = rootProject.libs.plugins.detekt.get().pluginId) 45 | extensions.configure { 46 | toolVersion = rootProject.libs.versions.detekt.get() 47 | config.setFrom(files("$rootDir/detekt.yml")) 48 | buildUponDefaultConfig = true 49 | ignoredBuildTypes = listOf("release") 50 | } 51 | dependencies { 52 | add("detektPlugins", rootProject.libs.detektFormatting) 53 | } 54 | tasks.withType().configureEach { 55 | jvmTarget = JvmTarget.JVM_11.target 56 | } 57 | } -------------------------------------------------------------------------------- /detekt.yml: -------------------------------------------------------------------------------- 1 | complexity: 2 | CyclomaticComplexMethod: 3 | ignoreSingleWhenExpression: true 4 | LongParameterList: 5 | ignoreAnnotated: ['Parcelize'] 6 | constructorThreshold: 8 7 | 8 | exceptions: 9 | TooGenericExceptionCaught: 10 | active: false 11 | 12 | formatting: 13 | android: true 14 | Indentation: 15 | active: false 16 | FinalNewline: 17 | insertFinalNewLine: false 18 | 19 | performance: 20 | SpreadOperator: 21 | active: false 22 | 23 | style: 24 | MagicNumber: 25 | active: false 26 | NewLineAtEndOfFile: 27 | active: false -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Set the build VMs heap size. 2 | # For more information about how Gradle memory options were chosen: https://github.com/android/nowinandroid/blob/689ef92e41427ab70f82e2c9fe59755441deae92/gradle.properties#L12 3 | org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8 -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=1 -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError 4 | kotlin.daemon.jvmargs=-Xmx3g -Dfile.encoding=UTF-8 -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=1 -XX:ReservedCodeCacheSize=320m -XX:+HeapDumpOnOutOfMemoryError 5 | # Controls whether Gradle should print a welcome message 6 | org.gradle.welcome=never 7 | # Allow usage of AndroidX instead of the old support libraries. 8 | android.useAndroidX=true 9 | # Disable Dokka Gradle plugin V1 deprecation warning until stable v2 is available 10 | org.jetbrains.dokka.experimental.gradle.pluginMode.nowarn=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | quickie = "1.11.0" 3 | 4 | androidconfig-minSdk = "21" 5 | androidconfig-compileSdk = "35" 6 | androidconfig-targetSdk = "35" 7 | androidconfig-buildTools = "36.0.0" 8 | 9 | androidGradle = "8.10.0" 10 | kotlin = "2.1.21" 11 | 12 | appcompat = "1.7.0" 13 | core = "1.16.0" 14 | 15 | cameraX = "1.4.2" 16 | 17 | barcodeScanning = "17.3.0" 18 | barcodeScanningGms = "18.3.1" 19 | 20 | materialDesign = "1.12.0" 21 | 22 | detekt = "1.23.8" 23 | dokka = "2.0.0" 24 | 25 | junit = "5.12.2" 26 | 27 | [libraries] 28 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } 29 | androidx-camera = { module = "androidx.camera:camera-camera2", version.ref = "cameraX" } 30 | androidx-cameraLifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraX" } 31 | androidx-cameraPreview = { module = "androidx.camera:camera-view", version.ref = "cameraX" } 32 | androidx-core = { module = "androidx.core:core-ktx", version.ref = "core" } 33 | 34 | mlkit-barcodeScanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcodeScanning" } 35 | mlkit-barcodeScanningGms = { module = "com.google.android.gms:play-services-mlkit-barcode-scanning", version.ref = "barcodeScanningGms" } 36 | 37 | google-materialDesign = { module = "com.google.android.material:material", version.ref = "materialDesign" } 38 | 39 | test-junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } 40 | test-junit-platformLauncher = { module = "org.junit.platform:junit-platform-launcher" } 41 | 42 | detektFormatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } 43 | 44 | [plugins] 45 | android-application = { id = "com.android.application", version.ref = "androidGradle" } 46 | android-library = { id = "com.android.library", version.ref = "androidGradle" } 47 | 48 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 49 | kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } 50 | kotlin-dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } 51 | 52 | detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/G00fY2/quickie/5b9cb9538c10617eae8ce2d7416c191e83860f02/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /quickie/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.kotlin.parcelize) 5 | alias(libs.plugins.kotlin.dokka) 6 | `maven-publish` 7 | signing 8 | } 9 | 10 | android { 11 | namespace = "io.github.g00fy2.quickie" 12 | resourcePrefix = "quickie" 13 | buildFeatures { 14 | viewBinding = true 15 | } 16 | flavorDimensions += "mlkit" 17 | productFlavors { 18 | create("bundled").dimension = "mlkit" 19 | create("unbundled").dimension = "mlkit" 20 | } 21 | sourceSets { 22 | getByName("bundled").java.srcDirs("src/bundled/kotlin") 23 | getByName("unbundled").java.srcDirs("src/unbundled/kotlin") 24 | } 25 | publishing { 26 | singleVariant("bundledRelease") 27 | singleVariant("unbundledRelease") 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation(libs.androidx.appcompat) 33 | implementation(libs.androidx.core) 34 | 35 | implementation(libs.androidx.camera) 36 | implementation(libs.androidx.cameraLifecycle) 37 | implementation(libs.androidx.cameraPreview) 38 | 39 | add("bundledImplementation", libs.mlkit.barcodeScanning) 40 | add("unbundledImplementation", libs.mlkit.barcodeScanningGms) 41 | 42 | testImplementation(libs.test.junit) 43 | testRuntimeOnly(libs.test.junit.platformLauncher) 44 | } 45 | 46 | group = "io.github.g00fy2.quickie" 47 | version = libs.versions.quickie.get() 48 | 49 | tasks.register("androidJavadocJar") { 50 | archiveClassifier = "javadoc" 51 | from(layout.buildDirectory.dir("dokka/javadoc")) 52 | dependsOn("dokkaJavadoc") 53 | } 54 | 55 | tasks.register("androidBundledSourcesJar") { 56 | archiveClassifier = "sources" 57 | from(android.sourceSets.getByName("main").java.srcDirs, android.sourceSets.getByName("bundled").java.srcDirs) 58 | } 59 | 60 | tasks.register("androidUnbundledSourcesJar") { 61 | archiveClassifier = "sources" 62 | from(android.sourceSets.getByName("main").java.srcDirs, android.sourceSets.getByName("unbundled").java.srcDirs) 63 | } 64 | 65 | afterEvaluate { 66 | tasks.withType().configureEach { 67 | useJUnitPlatform() 68 | testLogging.events("failed", "passed", "skipped") 69 | enabled = name.endsWith("DebugUnitTest") 70 | } 71 | 72 | publishing { 73 | publications { 74 | create("bundledRelease") { commonConfig("bundled") } 75 | create("unbundledRelease") { commonConfig("unbundled") } 76 | } 77 | repositories { 78 | maven { 79 | name = "sonatype" 80 | url = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") 81 | credentials { 82 | username = findStringProperty("sonatypeUsername") 83 | password = findStringProperty("sonatypePassword") 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | signing { 91 | findStringProperty("signing.keyId") 92 | findStringProperty("signing.password") 93 | findStringProperty("signing.secretKeyRingFile") 94 | sign(publishing.publications) 95 | } 96 | 97 | fun MavenPublication.commonConfig(flavor: String) { 98 | from(components["${flavor}Release"]) 99 | artifactId = "quickie-$flavor" 100 | artifact(tasks.named("androidJavadocJar")) 101 | artifact(tasks.named("android${flavor.replaceFirstChar { it.titlecase() }}SourcesJar")) 102 | pom { 103 | name = "quickie-$flavor" 104 | description = "Android QR code scanning library" 105 | url = "https://github.com/G00fY2/quickie" 106 | licenses { 107 | license { 108 | name = "MIT License" 109 | url = "https://opensource.org/licenses/MIT" 110 | } 111 | } 112 | developers { 113 | developer { 114 | id = "g00fy2" 115 | name = "Thomas Wirth" 116 | email = "twirth.development@gmail.com" 117 | } 118 | } 119 | scm { 120 | connection = "https://github.com/G00fY2/quickie.git" 121 | developerConnection = "https://github.com/G00fY2/quickie.git" 122 | url = "https://github.com/G00fY2/quickie" 123 | } 124 | } 125 | } 126 | 127 | fun Project.findStringProperty(propertyName: String): String? { 128 | return findProperty(propertyName) as String? ?: run { 129 | println("$propertyName missing in gradle.properties") 130 | null 131 | } 132 | } -------------------------------------------------------------------------------- /quickie/src/bundled/kotlin/io/github/g00fy2/quickie/utils/MlKitErrorHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie.utils 2 | 3 | import io.github.g00fy2.quickie.QRScannerActivity 4 | 5 | internal object MlKitErrorHandler { 6 | 7 | @Suppress("UNUSED_PARAMETER", "FunctionOnlyReturningConstant") 8 | fun isResolvableError(activity: QRScannerActivity, exception: Exception) = false // always false when bundled 9 | } -------------------------------------------------------------------------------- /quickie/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /quickie/src/main/kotlin/io/github/g00fy2/quickie/QRCodeAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie 2 | 3 | import androidx.camera.core.ExperimentalGetImage 4 | import androidx.camera.core.ImageAnalysis 5 | import androidx.camera.core.ImageProxy 6 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions 7 | import com.google.mlkit.vision.barcode.BarcodeScanning 8 | import com.google.mlkit.vision.barcode.common.Barcode 9 | import com.google.mlkit.vision.common.InputImage 10 | 11 | internal class QRCodeAnalyzer( 12 | private val barcodeFormats: IntArray, 13 | private val onSuccess: ((Barcode) -> Unit), 14 | private val onFailure: ((Exception) -> Unit), 15 | private val onPassCompleted: ((Boolean) -> Unit) 16 | ) : ImageAnalysis.Analyzer { 17 | 18 | private val barcodeScanner by lazy { 19 | val optionsBuilder = if (barcodeFormats.size > 1) { 20 | BarcodeScannerOptions.Builder().setBarcodeFormats(barcodeFormats.first(), *barcodeFormats.drop(1).toIntArray()) 21 | } else { 22 | BarcodeScannerOptions.Builder().setBarcodeFormats(barcodeFormats.firstOrNull() ?: Barcode.FORMAT_UNKNOWN) 23 | } 24 | try { 25 | BarcodeScanning.getClient(optionsBuilder.build()) 26 | } catch (e: Exception) { // catch if for some reason MlKitContext has not been initialized 27 | onFailure(e) 28 | null 29 | } 30 | } 31 | 32 | private var failureOccurred = false 33 | private var failureTimestamp = 0L 34 | 35 | @ExperimentalGetImage 36 | override fun analyze(imageProxy: ImageProxy) { 37 | val mediaImage = imageProxy.image 38 | 39 | // skip analysis if no media image or error occurred in previous pass 40 | if (mediaImage == null || (failureOccurred && System.currentTimeMillis() - failureTimestamp < 1000L)) { 41 | imageProxy.close() 42 | return 43 | } 44 | 45 | failureOccurred = false 46 | barcodeScanner?.let { scanner -> 47 | scanner.process(InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)) 48 | .addOnSuccessListener { codes -> codes.firstNotNullOfOrNull { it }?.let { onSuccess(it) } } 49 | .addOnFailureListener { 50 | failureOccurred = true 51 | failureTimestamp = System.currentTimeMillis() 52 | onFailure(it) 53 | } 54 | .addOnCompleteListener { 55 | onPassCompleted(failureOccurred) 56 | imageProxy.close() 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /quickie/src/main/kotlin/io/github/g00fy2/quickie/QROverlayView.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie 2 | 3 | import android.content.Context 4 | import android.content.res.ColorStateList 5 | import android.content.res.Resources.NotFoundException 6 | import android.graphics.Bitmap 7 | import android.graphics.Canvas 8 | import android.graphics.Color 9 | import android.graphics.Paint 10 | import android.graphics.PorterDuff.Mode.CLEAR 11 | import android.graphics.PorterDuffXfermode 12 | import android.graphics.RectF 13 | import android.graphics.drawable.Drawable 14 | import android.util.AttributeSet 15 | import android.util.TypedValue 16 | import android.view.LayoutInflater 17 | import android.view.View 18 | import android.widget.FrameLayout 19 | import androidx.core.content.ContextCompat 20 | import androidx.core.content.res.ResourcesCompat 21 | import androidx.core.graphics.ColorUtils 22 | import androidx.core.graphics.createBitmap 23 | import androidx.core.graphics.drawable.DrawableCompat 24 | import io.github.g00fy2.quickie.databinding.QuickieOverlayViewBinding 25 | import kotlin.math.min 26 | import kotlin.math.roundToInt 27 | 28 | @Suppress("TooManyFunctions") 29 | internal class QROverlayView @JvmOverloads constructor( 30 | context: Context, 31 | attrs: AttributeSet? = null, 32 | defStyleAttr: Int = 0 33 | ) : FrameLayout(context, attrs, defStyleAttr) { 34 | 35 | private val binding = QuickieOverlayViewBinding.inflate(LayoutInflater.from(context), this) 36 | private val grayColor = ContextCompat.getColor(context, R.color.quickie_gray) 37 | private val accentColor = getAccentColor() 38 | private val backgroundColor = ColorUtils.setAlphaComponent(Color.BLACK, BACKGROUND_ALPHA.roundToInt()) 39 | private val alphaPaint = Paint().apply { alpha = BACKGROUND_ALPHA.roundToInt() } 40 | private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG) 41 | private val loadingBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = backgroundColor } 42 | private val transparentPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { 43 | color = Color.TRANSPARENT 44 | xfermode = PorterDuffXfermode(CLEAR) 45 | } 46 | private val outerRadius = OUT_RADIUS.toPx() 47 | private val innerRadius = (OUT_RADIUS - STROKE_WIDTH).toPx() 48 | private val outerFrame = RectF() 49 | private val innerFrame = RectF() 50 | private var maskBitmap: Bitmap? = null 51 | private var maskCanvas: Canvas? = null 52 | private var horizontalFrameRatio = 1f 53 | var isHighlighted = false 54 | set(value) { 55 | if (field != value) { 56 | field = value 57 | invalidate() 58 | } 59 | } 60 | var isLoading = false 61 | set(value) { 62 | if (field != value) { 63 | field = value 64 | binding.progressView.visibility = if (value) View.VISIBLE else View.GONE 65 | } 66 | } 67 | 68 | init { 69 | setWillNotDraw(false) 70 | } 71 | 72 | override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 73 | super.onLayout(changed, left, top, right, bottom) 74 | 75 | if (maskBitmap == null && width > 0 && height > 0) { 76 | maskBitmap = createBitmap(width, height).apply { maskCanvas = Canvas(this) } 77 | calculateFrameAndTitlePos() 78 | } 79 | } 80 | 81 | @Suppress("UnsafeCallOnNullableType") 82 | override fun onDraw(canvas: Canvas) { 83 | strokePaint.color = if (isHighlighted) accentColor else grayColor 84 | maskCanvas!!.drawColor(backgroundColor) 85 | maskCanvas!!.drawRoundRect(outerFrame, outerRadius, outerRadius, strokePaint) 86 | maskCanvas!!.drawRoundRect(innerFrame, innerRadius, innerRadius, transparentPaint) 87 | if (isLoading) maskCanvas!!.drawRoundRect(innerFrame, innerRadius, innerRadius, loadingBackgroundPaint) 88 | canvas.drawBitmap(maskBitmap!!, 0f, 0f, alphaPaint) 89 | super.onDraw(canvas) 90 | } 91 | 92 | fun setCustomText(stringRes: Int) { 93 | if (stringRes != 0) { 94 | try { 95 | binding.titleTextView.setText(stringRes) 96 | } catch (ignore: NotFoundException) { 97 | // string resource not found 98 | } 99 | } 100 | } 101 | 102 | fun setCustomIcon(drawableRes: Int?) { 103 | if (drawableRes == null) { 104 | binding.titleTextView.setCompoundDrawables(null, null, null, null) 105 | } else if (drawableRes != 0) { 106 | try { 107 | ResourcesCompat.getDrawable(resources, drawableRes, null)?.limitDrawableSize()?.let { 108 | binding.titleTextView.setCompoundDrawables(null, it, null, null) 109 | } 110 | } catch (ignore: NotFoundException) { 111 | // drawable resource not found 112 | } 113 | } 114 | } 115 | 116 | fun setHorizontalFrameRatio(ratio: Float) { 117 | if (ratio > 1f) { 118 | horizontalFrameRatio = ratio 119 | calculateFrameAndTitlePos() 120 | } 121 | } 122 | 123 | fun setCloseVisibilityAndOnClick(visible: Boolean, action: () -> Unit = {}) { 124 | binding.closeImageView.visibility = if (visible) View.VISIBLE else View.GONE 125 | binding.closeImageView.setOnClickListener { action() } 126 | if (visible) binding.closeImageView.setTintAndStateAwareBackground() 127 | } 128 | 129 | fun setTorchVisibilityAndOnClick(visible: Boolean, action: (Boolean) -> Unit = {}) { 130 | binding.torchImageView.visibility = if (visible) View.VISIBLE else View.GONE 131 | binding.torchImageView.setOnClickListener { action(!it.isSelected) } 132 | if (visible) binding.torchImageView.setTintAndStateAwareBackground() 133 | } 134 | 135 | fun setTorchState(on: Boolean) { 136 | binding.torchImageView.isSelected = on 137 | } 138 | 139 | private fun calculateFrameAndTitlePos() { 140 | val centralX = width / 2 141 | val centralY = height / 2 142 | val minLength = min(centralX, centralY) 143 | val marginRatio = if (horizontalFrameRatio > 1f) { 144 | FRAME_MARGIN_RATIO * ((1f / horizontalFrameRatio) * 1.5f) 145 | } else { 146 | FRAME_MARGIN_RATIO 147 | } 148 | val strokeLength = minLength - (minLength * marginRatio) 149 | val strokeWidth = STROKE_WIDTH.toPx() 150 | outerFrame.set( 151 | centralX - strokeLength, 152 | centralY - strokeLength / horizontalFrameRatio, 153 | centralX + strokeLength, 154 | centralY + strokeLength / horizontalFrameRatio 155 | ) 156 | innerFrame.set( 157 | outerFrame.left + strokeWidth, 158 | outerFrame.top + strokeWidth, 159 | outerFrame.right - strokeWidth, 160 | outerFrame.bottom - strokeWidth 161 | ) 162 | 163 | val topInsetsToOuterFrame = (-paddingTop + centralY - strokeLength).roundToInt() 164 | val titleCenter = (topInsetsToOuterFrame - binding.titleTextView.height) / 2 165 | binding.titleTextView.updateTopMargin(titleCenter) 166 | // hide title text if not enough vertical space 167 | binding.titleTextView.visibility = 168 | if (topInsetsToOuterFrame < binding.titleTextView.height) View.INVISIBLE else View.VISIBLE 169 | } 170 | 171 | private fun getAccentColor(): Int { 172 | return TypedValue().let { 173 | if (context.theme.resolveAttribute(android.R.attr.colorAccent, it, true)) { 174 | it.data 175 | } else { 176 | ContextCompat.getColor(context, R.color.quickie_accent_fallback) 177 | } 178 | } 179 | } 180 | 181 | private fun View.updateTopMargin(topPx: Int) { 182 | val params = layoutParams as MarginLayoutParams 183 | params.topMargin = topPx 184 | layoutParams = params 185 | } 186 | 187 | private fun Drawable.limitDrawableSize(): Drawable { 188 | val heightLimit = ICON_MAX_HEIGHT.toPx() 189 | val scale = heightLimit / minimumHeight 190 | if (scale < 1) { 191 | setBounds(0, 0, (minimumWidth * scale).roundToInt(), (minimumHeight * scale).roundToInt()) 192 | } else { 193 | setBounds(0, 0, minimumWidth, minimumHeight) 194 | } 195 | return this 196 | } 197 | 198 | private fun View.setTintAndStateAwareBackground() { 199 | background?.let { drawable -> 200 | val wrappedDrawable = DrawableCompat.wrap(drawable) 201 | 202 | val states = arrayOf( 203 | intArrayOf(android.R.attr.state_pressed, android.R.attr.state_selected), 204 | intArrayOf(android.R.attr.state_pressed, -android.R.attr.state_selected), 205 | intArrayOf(-android.R.attr.state_pressed, android.R.attr.state_selected), 206 | intArrayOf() 207 | ) 208 | val stateColors = intArrayOf(grayColor, accentColor, accentColor, grayColor) 209 | val colorStateList = ColorStateList(states, stateColors).withAlpha(BUTTON_BACKGROUND_ALPHA.roundToInt()) 210 | 211 | DrawableCompat.setTintList(wrappedDrawable, colorStateList) 212 | background = wrappedDrawable 213 | } 214 | } 215 | 216 | private fun Float.toPx() = 217 | TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, resources.displayMetrics) 218 | 219 | companion object { 220 | private const val BACKGROUND_ALPHA = 0.77 * 255 221 | private const val BUTTON_BACKGROUND_ALPHA = 0.6 * 255 222 | private const val STROKE_WIDTH = 4f 223 | private const val OUT_RADIUS = 16f 224 | private const val FRAME_MARGIN_RATIO = 1f / 4 225 | private const val ICON_MAX_HEIGHT = 56f 226 | } 227 | } -------------------------------------------------------------------------------- /quickie/src/main/kotlin/io/github/g00fy2/quickie/QRResult.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie 2 | 3 | import io.github.g00fy2.quickie.content.QRContent 4 | 5 | public sealed class QRResult { 6 | 7 | /** 8 | * MLKit successfully detected a QR code. 9 | * 10 | * @property content the wrapped MLKit response. 11 | */ 12 | public data class QRSuccess(val content: QRContent) : QRResult() 13 | 14 | /** 15 | * Activity got cancelled by the user. 16 | */ 17 | public data object QRUserCanceled : QRResult() 18 | 19 | /** 20 | * Camera permission was not granted. 21 | */ 22 | public data object QRMissingPermission : QRResult() 23 | 24 | /** 25 | * Error while setting up CameraX or while MLKit analysis. 26 | * 27 | * @property exception the cause why the Activity was finished. 28 | */ 29 | public data class QRError(val exception: Exception) : QRResult() 30 | } -------------------------------------------------------------------------------- /quickie/src/main/kotlin/io/github/g00fy2/quickie/QRScannerActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie 2 | 3 | import android.Manifest.permission.CAMERA 4 | import android.app.Activity 5 | import android.app.Dialog 6 | import android.content.Intent 7 | import android.content.pm.PackageManager 8 | import android.os.Bundle 9 | import android.util.Size 10 | import android.view.HapticFeedbackConstants 11 | import android.view.KeyEvent 12 | import android.view.View 13 | import android.view.WindowManager 14 | import androidx.activity.result.contract.ActivityResultContracts 15 | import androidx.appcompat.app.AppCompatActivity 16 | import androidx.appcompat.view.ContextThemeWrapper 17 | import androidx.camera.core.CameraSelector 18 | import androidx.camera.core.ImageAnalysis 19 | import androidx.camera.core.Preview 20 | import androidx.camera.core.TorchState 21 | import androidx.camera.core.resolutionselector.ResolutionSelector 22 | import androidx.camera.core.resolutionselector.ResolutionStrategy 23 | import androidx.camera.lifecycle.ProcessCameraProvider 24 | import androidx.core.content.ContextCompat 25 | import androidx.core.content.IntentCompat 26 | import androidx.core.view.ViewCompat 27 | import androidx.core.view.WindowCompat 28 | import androidx.core.view.WindowInsetsCompat 29 | import com.google.mlkit.vision.barcode.common.Barcode 30 | import io.github.g00fy2.quickie.config.ParcelableScannerConfig 31 | import io.github.g00fy2.quickie.databinding.QuickieScannerActivityBinding 32 | import io.github.g00fy2.quickie.extensions.toParcelableContentType 33 | import io.github.g00fy2.quickie.utils.MlKitErrorHandler 34 | import java.util.concurrent.ExecutorService 35 | import java.util.concurrent.Executors 36 | 37 | internal class QRScannerActivity : AppCompatActivity() { 38 | 39 | private lateinit var binding: QuickieScannerActivityBinding 40 | private lateinit var analysisExecutor: ExecutorService 41 | private var barcodeFormats = intArrayOf(Barcode.FORMAT_QR_CODE) 42 | private var hapticFeedback = true 43 | private var showTorchToggle = false 44 | private var showCloseButton = false 45 | private var useFrontCamera = false 46 | internal var errorDialog: Dialog? = null 47 | set(value) { 48 | field = value 49 | value?.show() 50 | value?.setOnKeyListener { dialog, keyCode, _ -> 51 | if (keyCode == KeyEvent.KEYCODE_BACK) { 52 | finish() 53 | dialog.dismiss() 54 | true 55 | } else { 56 | false 57 | } 58 | } 59 | } 60 | 61 | override fun onCreate(savedInstanceState: Bundle?) { 62 | super.onCreate(savedInstanceState) 63 | val appThemeLayoutInflater = applicationInfo.theme.let { appThemeRes -> 64 | if (appThemeRes != 0) layoutInflater.cloneInContext(ContextThemeWrapper(this, appThemeRes)) else layoutInflater 65 | } 66 | binding = QuickieScannerActivityBinding.inflate(appThemeLayoutInflater) 67 | setContentView(binding.root) 68 | 69 | setupEdgeToEdgeUI() 70 | applyScannerConfig() 71 | 72 | analysisExecutor = Executors.newSingleThreadExecutor() 73 | 74 | requestCameraPermissionIfMissing { granted -> 75 | if (granted) { 76 | startCamera() 77 | } else { 78 | setResult(RESULT_MISSING_PERMISSION, null) 79 | finish() 80 | } 81 | } 82 | } 83 | 84 | override fun onDestroy() { 85 | super.onDestroy() 86 | analysisExecutor.shutdown() 87 | } 88 | 89 | private fun startCamera() { 90 | val cameraProviderFuture = try { 91 | ProcessCameraProvider.getInstance(this) 92 | } catch (e: Exception) { 93 | onFailure(e) 94 | return 95 | } 96 | 97 | cameraProviderFuture.addListener({ 98 | val cameraProvider = try { 99 | cameraProviderFuture.get() 100 | } catch (e: Exception) { 101 | onFailure(e) 102 | return@addListener 103 | } 104 | 105 | val preview = Preview.Builder().build().also { it.surfaceProvider = binding.previewView.surfaceProvider } 106 | val imageAnalysis = ImageAnalysis.Builder() 107 | .setResolutionSelector( 108 | ResolutionSelector.Builder().setResolutionStrategy( 109 | ResolutionStrategy( 110 | Size(1280, 720), 111 | ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER 112 | ) 113 | ).build() 114 | ) 115 | .build() 116 | .also { 117 | it.setAnalyzer( 118 | analysisExecutor, 119 | QRCodeAnalyzer( 120 | barcodeFormats = barcodeFormats, 121 | onSuccess = { barcode -> 122 | it.clearAnalyzer() 123 | onSuccess(barcode) 124 | }, 125 | onFailure = { exception -> onFailure(exception) }, 126 | onPassCompleted = { failureOccurred -> onPassCompleted(failureOccurred) } 127 | ) 128 | ) 129 | } 130 | 131 | cameraProvider.unbindAll() 132 | 133 | val cameraSelector = 134 | if (useFrontCamera) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA 135 | 136 | try { 137 | val camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis) 138 | binding.overlayView.visibility = View.VISIBLE 139 | binding.overlayView.setCloseVisibilityAndOnClick(showCloseButton) { finish() } 140 | if (showTorchToggle && camera.cameraInfo.hasFlashUnit()) { 141 | binding.overlayView.setTorchVisibilityAndOnClick(true) { camera.cameraControl.enableTorch(it) } 142 | camera.cameraInfo.torchState.observe(this) { binding.overlayView.setTorchState(it == TorchState.ON) } 143 | } else { 144 | binding.overlayView.setTorchVisibilityAndOnClick(false) 145 | } 146 | } catch (e: Exception) { 147 | binding.overlayView.visibility = View.INVISIBLE 148 | onFailure(e) 149 | } 150 | }, ContextCompat.getMainExecutor(this)) 151 | } 152 | 153 | private fun onSuccess(result: Barcode) { 154 | binding.overlayView.isHighlighted = true 155 | if (hapticFeedback) { 156 | @Suppress("DEPRECATION") 157 | val flags = HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING or HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING 158 | binding.overlayView.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP, flags) 159 | } 160 | setResult( 161 | Activity.RESULT_OK, 162 | Intent().apply { 163 | putExtra(EXTRA_RESULT_BYTES, result.rawBytes) 164 | putExtra(EXTRA_RESULT_VALUE, result.rawValue) 165 | putExtra(EXTRA_RESULT_TYPE, result.valueType) 166 | putExtra(EXTRA_RESULT_PARCELABLE, result.toParcelableContentType()) 167 | } 168 | ) 169 | finish() 170 | } 171 | 172 | private fun onFailure(exception: Exception) { 173 | setResult(RESULT_ERROR, Intent().putExtra(EXTRA_RESULT_EXCEPTION, exception)) 174 | if (!MlKitErrorHandler.isResolvableError(this, exception)) finish() 175 | } 176 | 177 | private fun onPassCompleted(failureOccurred: Boolean) { 178 | if (!isFinishing) binding.overlayView.isLoading = failureOccurred 179 | } 180 | 181 | private fun setupEdgeToEdgeUI() { 182 | WindowCompat.setDecorFitsSystemWindows(window, false) 183 | ViewCompat.setOnApplyWindowInsetsListener(binding.overlayView) { v, insets -> 184 | insets.getInsets(WindowInsetsCompat.Type.systemBars()).let { v.setPadding(it.left, it.top, it.right, it.bottom) } 185 | WindowInsetsCompat.CONSUMED 186 | } 187 | } 188 | 189 | private fun applyScannerConfig() { 190 | intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_CONFIG, ParcelableScannerConfig::class.java) }?.let { 191 | barcodeFormats = it.formats 192 | binding.overlayView.setCustomText(it.stringRes) 193 | binding.overlayView.setCustomIcon(it.drawableRes) 194 | binding.overlayView.setHorizontalFrameRatio(it.horizontalFrameRatio) 195 | hapticFeedback = it.hapticFeedback 196 | showTorchToggle = it.showTorchToggle 197 | useFrontCamera = it.useFrontCamera 198 | showCloseButton = it.showCloseButton 199 | 200 | if (it.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 201 | } 202 | } 203 | 204 | private fun requestCameraPermissionIfMissing(onResult: ((Boolean) -> Unit)) { 205 | if (ContextCompat.checkSelfPermission(this, CAMERA) == PackageManager.PERMISSION_GRANTED) { 206 | onResult(true) 207 | } else { 208 | registerForActivityResult(ActivityResultContracts.RequestPermission()) { onResult(it) }.launch(CAMERA) 209 | } 210 | } 211 | 212 | companion object { 213 | const val EXTRA_CONFIG = "quickie-config" 214 | const val EXTRA_RESULT_BYTES = "quickie-bytes" 215 | const val EXTRA_RESULT_VALUE = "quickie-value" 216 | const val EXTRA_RESULT_TYPE = "quickie-type" 217 | const val EXTRA_RESULT_PARCELABLE = "quickie-parcelable" 218 | const val EXTRA_RESULT_EXCEPTION = "quickie-exception" 219 | const val RESULT_MISSING_PERMISSION = RESULT_FIRST_USER + 1 220 | const val RESULT_ERROR = RESULT_FIRST_USER + 2 221 | } 222 | } -------------------------------------------------------------------------------- /quickie/src/main/kotlin/io/github/g00fy2/quickie/ScanCustomCode.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie 2 | 3 | import android.app.Activity.RESULT_CANCELED 4 | import android.app.Activity.RESULT_OK 5 | import android.content.Context 6 | import android.content.Intent 7 | import androidx.activity.result.contract.ActivityResultContract 8 | import io.github.g00fy2.quickie.QRResult.QRError 9 | import io.github.g00fy2.quickie.QRResult.QRMissingPermission 10 | import io.github.g00fy2.quickie.QRResult.QRSuccess 11 | import io.github.g00fy2.quickie.QRResult.QRUserCanceled 12 | import io.github.g00fy2.quickie.QRScannerActivity.Companion.EXTRA_CONFIG 13 | import io.github.g00fy2.quickie.QRScannerActivity.Companion.RESULT_ERROR 14 | import io.github.g00fy2.quickie.QRScannerActivity.Companion.RESULT_MISSING_PERMISSION 15 | import io.github.g00fy2.quickie.config.ScannerConfig 16 | import io.github.g00fy2.quickie.extensions.getRootException 17 | import io.github.g00fy2.quickie.extensions.toParcelableConfig 18 | import io.github.g00fy2.quickie.extensions.toQuickieContentType 19 | 20 | public class ScanCustomCode : ActivityResultContract() { 21 | 22 | override fun createIntent(context: Context, input: ScannerConfig): Intent { 23 | return Intent(context, QRScannerActivity::class.java).apply { 24 | putExtra(EXTRA_CONFIG, input.toParcelableConfig()) 25 | } 26 | } 27 | 28 | override fun parseResult(resultCode: Int, intent: Intent?): QRResult { 29 | return when (resultCode) { 30 | RESULT_OK -> QRSuccess(intent.toQuickieContentType()) 31 | RESULT_CANCELED -> QRUserCanceled 32 | RESULT_MISSING_PERMISSION -> QRMissingPermission 33 | RESULT_ERROR -> QRError(intent.getRootException()) 34 | else -> QRError(IllegalStateException("Unknown activity result code $resultCode")) 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /quickie/src/main/kotlin/io/github/g00fy2/quickie/ScanQRCode.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie 2 | 3 | import android.app.Activity.RESULT_CANCELED 4 | import android.app.Activity.RESULT_OK 5 | import android.content.Context 6 | import android.content.Intent 7 | import androidx.activity.result.contract.ActivityResultContract 8 | import io.github.g00fy2.quickie.QRResult.QRError 9 | import io.github.g00fy2.quickie.QRResult.QRMissingPermission 10 | import io.github.g00fy2.quickie.QRResult.QRSuccess 11 | import io.github.g00fy2.quickie.QRResult.QRUserCanceled 12 | import io.github.g00fy2.quickie.QRScannerActivity.Companion.RESULT_ERROR 13 | import io.github.g00fy2.quickie.QRScannerActivity.Companion.RESULT_MISSING_PERMISSION 14 | import io.github.g00fy2.quickie.extensions.getRootException 15 | import io.github.g00fy2.quickie.extensions.toQuickieContentType 16 | 17 | public class ScanQRCode : ActivityResultContract() { 18 | 19 | override fun createIntent(context: Context, input: Nothing?): Intent = 20 | Intent(context, QRScannerActivity::class.java) 21 | 22 | override fun parseResult(resultCode: Int, intent: Intent?): QRResult { 23 | return when (resultCode) { 24 | RESULT_OK -> QRSuccess(intent.toQuickieContentType()) 25 | RESULT_CANCELED -> QRUserCanceled 26 | RESULT_MISSING_PERMISSION -> QRMissingPermission 27 | RESULT_ERROR -> QRError(intent.getRootException()) 28 | else -> QRError(IllegalStateException("Unknown activity result code $resultCode")) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /quickie/src/main/kotlin/io/github/g00fy2/quickie/config/BarcodeFormat.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie.config 2 | 3 | import com.google.mlkit.vision.barcode.common.Barcode 4 | 5 | /** 6 | * Wrapper class to access the ML Kit BarcodeFormat constants. 7 | */ 8 | public enum class BarcodeFormat(internal val value: Int) { 9 | FORMAT_ALL_FORMATS(Barcode.FORMAT_ALL_FORMATS), 10 | FORMAT_CODE_128(Barcode.FORMAT_CODE_128), 11 | FORMAT_CODE_39(Barcode.FORMAT_CODE_39), 12 | FORMAT_CODE_93(Barcode.FORMAT_CODE_93), 13 | FORMAT_CODABAR(Barcode.FORMAT_CODABAR), 14 | FORMAT_DATA_MATRIX(Barcode.FORMAT_DATA_MATRIX), 15 | FORMAT_EAN_13(Barcode.FORMAT_EAN_13), 16 | FORMAT_EAN_8(Barcode.FORMAT_EAN_8), 17 | FORMAT_ITF(Barcode.FORMAT_ITF), 18 | FORMAT_QR_CODE(Barcode.FORMAT_QR_CODE), 19 | FORMAT_UPC_A(Barcode.FORMAT_UPC_A), 20 | FORMAT_UPC_E(Barcode.FORMAT_UPC_E), 21 | FORMAT_PDF417(Barcode.FORMAT_PDF417), 22 | FORMAT_AZTEC(Barcode.FORMAT_AZTEC) 23 | } -------------------------------------------------------------------------------- /quickie/src/main/kotlin/io/github/g00fy2/quickie/config/ParcelableScannerConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie.config 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | internal class ParcelableScannerConfig( 8 | val formats: IntArray, 9 | val stringRes: Int, 10 | val drawableRes: Int?, 11 | val hapticFeedback: Boolean, 12 | val showTorchToggle: Boolean, 13 | val horizontalFrameRatio: Float, 14 | val useFrontCamera: Boolean, 15 | val showCloseButton: Boolean, 16 | val keepScreenOn: Boolean, 17 | ) : Parcelable -------------------------------------------------------------------------------- /quickie/src/main/kotlin/io/github/g00fy2/quickie/config/ScannerConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie.config 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | 6 | /** 7 | * Builder for ScannerConfig used in ScanBarcode ActivityResultContract. 8 | */ 9 | @Suppress("LongParameterList") 10 | public class ScannerConfig( 11 | internal val formats: IntArray, 12 | internal val stringRes: Int, 13 | internal val drawableRes: Int?, 14 | internal val hapticFeedback: Boolean, 15 | internal val showTorchToggle: Boolean, 16 | internal val horizontalFrameRatio: Float, 17 | internal val useFrontCamera: Boolean, 18 | internal val showCloseButton: Boolean, 19 | internal val keepScreenOn: Boolean, 20 | ) { 21 | 22 | public class Builder { 23 | private var barcodeFormats: List = listOf(BarcodeFormat.FORMAT_ALL_FORMATS) 24 | private var overlayStringRes: Int = 0 25 | private var overlayDrawableRes: Int? = 0 26 | private var hapticSuccessFeedback: Boolean = true 27 | private var showTorchToggle: Boolean = false 28 | private var horizontalFrameRatio: Float = 1f 29 | private var useFrontCamera: Boolean = false 30 | private var showCloseButton: Boolean = false 31 | private var keepScreenOn: Boolean = false 32 | 33 | /** 34 | * Set a list of interested barcode formats. List must not be empty. 35 | * Reducing the number of supported formats will make the barcode scanner faster. 36 | */ 37 | public fun setBarcodeFormats(formats: List): Builder = apply { barcodeFormats = formats } 38 | 39 | /** 40 | * Set a string resource used for the scanner overlay. 41 | */ 42 | public fun setOverlayStringRes(@StringRes stringRes: Int): Builder = apply { overlayStringRes = stringRes } 43 | 44 | /** 45 | * Set a drawable resource used for the scanner overlay. 46 | * If null is passed, no icon will be shown. 47 | */ 48 | public fun setOverlayDrawableRes(@DrawableRes drawableRes: Int?): Builder = 49 | apply { overlayDrawableRes = drawableRes } 50 | 51 | /** 52 | * Set the horizontal overlay ratio (default is 1 / square frame). 53 | */ 54 | public fun setHorizontalFrameRatio(ratio: Float): Builder = apply { horizontalFrameRatio = ratio } 55 | 56 | /** 57 | * Enable (default) or disable haptic feedback when a barcode was detected. 58 | */ 59 | public fun setHapticSuccessFeedback(enable: Boolean): Builder = apply { hapticSuccessFeedback = enable } 60 | 61 | /** 62 | * Show or hide (default) torch/flashlight toggle button. 63 | */ 64 | public fun setShowTorchToggle(enable: Boolean): Builder = apply { showTorchToggle = enable } 65 | 66 | /** 67 | * Use the front camera. 68 | */ 69 | public fun setUseFrontCamera(enable: Boolean): Builder = apply { useFrontCamera = enable } 70 | 71 | /** 72 | * Show or hide (default) close button. 73 | */ 74 | public fun setShowCloseButton(enable: Boolean): Builder = apply { showCloseButton = enable } 75 | 76 | /** 77 | * Keep the device's screen turned on as long as the scanner is visible. 78 | */ 79 | public fun setKeepScreenOn(enable: Boolean): Builder = apply { keepScreenOn = enable } 80 | 81 | /** 82 | * Build the BarcodeConfig required by the ScanBarcode ActivityResultContract. 83 | */ 84 | public fun build(): ScannerConfig = 85 | ScannerConfig( 86 | formats = barcodeFormats.map { it.value }.toIntArray(), 87 | stringRes = overlayStringRes, 88 | drawableRes = overlayDrawableRes, 89 | hapticFeedback = hapticSuccessFeedback, 90 | showTorchToggle = showTorchToggle, 91 | horizontalFrameRatio = horizontalFrameRatio, 92 | useFrontCamera = useFrontCamera, 93 | showCloseButton = showCloseButton, 94 | keepScreenOn = keepScreenOn, 95 | ) 96 | } 97 | 98 | public companion object { 99 | /** 100 | * Kotlin friendly method to build the BarcodeConfig required by the ScanBarcode ActivityResultContract. 101 | */ 102 | public fun build(func: Builder.() -> Unit): ScannerConfig = Builder().apply { func() }.build() 103 | } 104 | } -------------------------------------------------------------------------------- /quickie/src/main/kotlin/io/github/g00fy2/quickie/content/ParcelableContent.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie.content 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | internal class WifiParcelable(val encryptionType: Int, val password: String, val ssid: String) : Parcelable 8 | 9 | @Parcelize 10 | internal class UrlBookmarkParcelable(val title: String, val url: String) : Parcelable 11 | 12 | @Parcelize 13 | internal class SmsParcelable(val message: String, val phoneNumber: String) : Parcelable 14 | 15 | @Parcelize 16 | internal class GeoPointParcelable(val lat: Double, val lng: Double) : Parcelable 17 | 18 | @Parcelize 19 | internal class ContactInfoParcelable( 20 | val addressParcelables: List, 21 | val emailParcelables: List, 22 | val nameParcelable: PersonNameParcelable, 23 | val organization: String, 24 | val phoneParcelables: List, 25 | val title: String, 26 | val urls: List 27 | ) : Parcelable 28 | 29 | @Parcelize 30 | internal class EmailParcelable(val address: String, val body: String, val subject: String, val type: Int) : 31 | Parcelable 32 | 33 | @Parcelize 34 | internal class PhoneParcelable(val number: String, val type: Int) : Parcelable 35 | 36 | @Parcelize 37 | internal class PersonNameParcelable( 38 | val first: String, 39 | val formattedName: String, 40 | val last: String, 41 | val middle: String, 42 | val prefix: String, 43 | val pronunciation: String, 44 | val suffix: String 45 | ) : Parcelable 46 | 47 | @Parcelize 48 | internal class CalendarEventParcelable( 49 | val description: String, 50 | val end: CalendarDateTimeParcelable, 51 | val location: String, 52 | val organizer: String, 53 | val start: CalendarDateTimeParcelable, 54 | val status: String, 55 | val summary: String 56 | ) : Parcelable 57 | 58 | @Parcelize 59 | internal class CalendarDateTimeParcelable( 60 | val day: Int, 61 | val hours: Int, 62 | val minutes: Int, 63 | val month: Int, 64 | val seconds: Int, 65 | val year: Int, 66 | val utc: Boolean 67 | ) : Parcelable 68 | 69 | @Parcelize 70 | internal class AddressParcelable(val addressLines: List, val type: Int) : Parcelable -------------------------------------------------------------------------------- /quickie/src/main/kotlin/io/github/g00fy2/quickie/content/QRContent.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie.content 2 | 3 | @Suppress("ArrayInDataClass") 4 | public sealed class QRContent( 5 | public open val rawBytes: ByteArray?, 6 | public open val rawValue: String?, 7 | ) { 8 | 9 | /** 10 | * Plain text or unknown content QR Code type. 11 | */ 12 | public data class Plain( 13 | override val rawBytes: ByteArray?, 14 | override val rawValue: String? 15 | ) : QRContent(rawBytes, rawValue) 16 | 17 | /** 18 | * Wi-Fi access point details from a 'WIFI:' or similar QR Code type. 19 | */ 20 | public data class Wifi( 21 | override val rawBytes: ByteArray?, 22 | override val rawValue: String?, 23 | val encryptionType: Int, 24 | val password: String, 25 | val ssid: String 26 | ) : QRContent(rawBytes, rawValue) 27 | 28 | /** 29 | * A URL or URL bookmark from a 'MEBKM:' or similar QR Code type. 30 | */ 31 | public data class Url( 32 | override val rawBytes: ByteArray?, 33 | override val rawValue: String?, 34 | val title: String, 35 | val url: String 36 | ) : QRContent(rawBytes, rawValue) 37 | 38 | /** 39 | * An SMS message from an 'SMS:' or similar QR Code type. 40 | */ 41 | public data class Sms( 42 | override val rawBytes: ByteArray?, 43 | override val rawValue: String?, 44 | val message: String, 45 | val phoneNumber: String 46 | ) : QRContent(rawBytes, rawValue) 47 | 48 | /** 49 | * GPS coordinates from a 'GEO:' or similar QR Code type. 50 | */ 51 | public data class GeoPoint( 52 | override val rawBytes: ByteArray?, 53 | override val rawValue: String?, 54 | val lat: Double, 55 | val lng: Double 56 | ) : QRContent(rawBytes, rawValue) 57 | 58 | /** 59 | * An email message from a 'MAILTO:' or similar QR Code type. 60 | */ 61 | public data class Email( 62 | override val rawBytes: ByteArray?, 63 | override val rawValue: String?, 64 | val address: String, 65 | val body: String, 66 | val subject: String, 67 | val type: EmailType 68 | ) : QRContent(rawBytes, rawValue) { 69 | public enum class EmailType { 70 | UNKNOWN, WORK, HOME 71 | } 72 | } 73 | 74 | /** 75 | * A phone number from a 'TEL:' or similar QR Code type. 76 | */ 77 | public data class Phone( 78 | override val rawBytes: ByteArray?, 79 | override val rawValue: String?, 80 | val number: String, 81 | val type: PhoneType 82 | ) : QRContent(rawBytes, rawValue) { 83 | public enum class PhoneType { 84 | UNKNOWN, WORK, HOME, FAX, MOBILE 85 | } 86 | } 87 | 88 | /** 89 | * A person's or organization's business card. 90 | */ 91 | public data class ContactInfo( 92 | override val rawBytes: ByteArray?, 93 | override val rawValue: String?, 94 | val addresses: List
, 95 | val emails: List, 96 | val name: PersonName, 97 | val organization: String, 98 | val phones: List, 99 | val title: String, 100 | val urls: List 101 | ) : QRContent(rawBytes, rawValue) { 102 | 103 | public data class Address(val addressLines: List, val type: AddressType) { 104 | public enum class AddressType { 105 | UNKNOWN, WORK, HOME 106 | } 107 | } 108 | 109 | public data class PersonName( 110 | val first: String, 111 | val formattedName: String, 112 | val last: String, 113 | val middle: String, 114 | val prefix: String, 115 | val pronunciation: String, 116 | val suffix: String 117 | ) 118 | } 119 | 120 | /** 121 | * A calendar event extracted from a QR Code. 122 | */ 123 | public data class CalendarEvent( 124 | override val rawBytes: ByteArray?, 125 | override val rawValue: String?, 126 | val description: String, 127 | val end: CalendarDateTime, 128 | val location: String, 129 | val organizer: String, 130 | val start: CalendarDateTime, 131 | val status: String, 132 | val summary: String 133 | ) : QRContent(rawBytes, rawValue) { 134 | 135 | public data class CalendarDateTime( 136 | val day: Int, 137 | val hours: Int, 138 | val minutes: Int, 139 | val month: Int, 140 | val seconds: Int, 141 | val year: Int, 142 | val utc: Boolean 143 | ) 144 | } 145 | } -------------------------------------------------------------------------------- /quickie/src/main/kotlin/io/github/g00fy2/quickie/extensions/BarcodeExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie.extensions 2 | 3 | import android.os.Parcelable 4 | import com.google.mlkit.vision.barcode.common.Barcode 5 | import io.github.g00fy2.quickie.content.AddressParcelable 6 | import io.github.g00fy2.quickie.content.CalendarDateTimeParcelable 7 | import io.github.g00fy2.quickie.content.CalendarEventParcelable 8 | import io.github.g00fy2.quickie.content.ContactInfoParcelable 9 | import io.github.g00fy2.quickie.content.EmailParcelable 10 | import io.github.g00fy2.quickie.content.GeoPointParcelable 11 | import io.github.g00fy2.quickie.content.PersonNameParcelable 12 | import io.github.g00fy2.quickie.content.PhoneParcelable 13 | import io.github.g00fy2.quickie.content.SmsParcelable 14 | import io.github.g00fy2.quickie.content.UrlBookmarkParcelable 15 | import io.github.g00fy2.quickie.content.WifiParcelable 16 | 17 | internal fun Barcode.toParcelableContentType(): Parcelable? { 18 | return when (valueType) { 19 | Barcode.TYPE_CONTACT_INFO -> { 20 | ContactInfoParcelable( 21 | addressParcelables = contactInfo?.addresses?.map { it.toParcelableAddress() }.orEmpty(), 22 | emailParcelables = contactInfo?.emails?.map { it.toParcelableEmail() }.orEmpty(), 23 | nameParcelable = contactInfo?.name.toParcelablePersonName(), 24 | organization = contactInfo?.organization.orEmpty(), 25 | phoneParcelables = contactInfo?.phones?.map { it.toParcelablePhone() }.orEmpty(), 26 | title = contactInfo?.title.orEmpty(), 27 | urls = contactInfo?.urls?.mapNotNull { it }.orEmpty() 28 | ) 29 | } 30 | Barcode.TYPE_EMAIL -> { 31 | EmailParcelable( 32 | address = email?.address.orEmpty(), 33 | body = email?.body.orEmpty(), 34 | subject = email?.subject.orEmpty(), 35 | type = email?.type ?: 0 36 | ) 37 | } 38 | Barcode.TYPE_PHONE -> PhoneParcelable(number = phone?.number.orEmpty(), type = phone?.type ?: 0) 39 | Barcode.TYPE_SMS -> SmsParcelable(message = sms?.message.orEmpty(), phoneNumber = sms?.phoneNumber.orEmpty()) 40 | Barcode.TYPE_URL -> UrlBookmarkParcelable(title = url?.title.orEmpty(), url = url?.url.orEmpty()) 41 | Barcode.TYPE_WIFI -> { 42 | WifiParcelable( 43 | encryptionType = wifi?.encryptionType ?: 0, 44 | password = wifi?.password.orEmpty(), 45 | ssid = wifi?.ssid.orEmpty() 46 | ) 47 | } 48 | Barcode.TYPE_GEO -> GeoPointParcelable(lat = geoPoint?.lat ?: 0.0, lng = geoPoint?.lng ?: 0.0) 49 | Barcode.TYPE_CALENDAR_EVENT -> { 50 | CalendarEventParcelable( 51 | description = calendarEvent?.description.orEmpty(), 52 | end = calendarEvent?.end.toParcelableCalendarEvent(), 53 | location = calendarEvent?.location.orEmpty(), 54 | organizer = calendarEvent?.organizer.orEmpty(), 55 | start = calendarEvent?.start.toParcelableCalendarEvent(), 56 | status = calendarEvent?.status.orEmpty(), 57 | summary = calendarEvent?.summary.orEmpty() 58 | ) 59 | } 60 | else -> null // TYPE_TEXT, TYPE_ISBN, TYPE_PRODUCT, TYPE_DRIVER_LICENSE, TYPE_UNKNOWN 61 | } 62 | } 63 | 64 | private fun Barcode.Address?.toParcelableAddress() = 65 | AddressParcelable( 66 | addressLines = this?.addressLines?.toList()?.mapNotNull { it }.orEmpty(), 67 | type = this?.type ?: 0 68 | ) 69 | 70 | private fun Barcode.Phone?.toParcelablePhone() = 71 | PhoneParcelable(number = this?.number.orEmpty(), type = this?.type ?: 0) 72 | 73 | private fun Barcode.PersonName?.toParcelablePersonName() = 74 | PersonNameParcelable( 75 | first = this?.first.orEmpty(), 76 | formattedName = this?.formattedName.orEmpty(), 77 | last = this?.last.orEmpty(), 78 | middle = this?.middle.orEmpty(), 79 | prefix = this?.prefix.orEmpty(), 80 | pronunciation = this?.pronunciation.orEmpty(), 81 | suffix = this?.suffix.orEmpty() 82 | ) 83 | 84 | private fun Barcode.Email?.toParcelableEmail() = 85 | EmailParcelable( 86 | address = this?.address.orEmpty(), 87 | body = this?.body.orEmpty(), 88 | subject = this?.subject.orEmpty(), 89 | type = this?.type ?: 0 90 | ) 91 | 92 | private fun Barcode.CalendarDateTime?.toParcelableCalendarEvent() = 93 | CalendarDateTimeParcelable( 94 | day = this?.day ?: -1, 95 | hours = this?.hours ?: -1, 96 | minutes = this?.minutes ?: -1, 97 | month = this?.month ?: -1, 98 | seconds = this?.seconds ?: -1, 99 | year = this?.year ?: -1, 100 | utc = this?.isUtc ?: false 101 | ) -------------------------------------------------------------------------------- /quickie/src/main/kotlin/io/github/g00fy2/quickie/extensions/IntentExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie.extensions 2 | 3 | import android.content.Intent 4 | import androidx.core.content.IntentCompat 5 | import com.google.mlkit.vision.barcode.common.Barcode 6 | import io.github.g00fy2.quickie.QRScannerActivity 7 | import io.github.g00fy2.quickie.QRScannerActivity.Companion.EXTRA_RESULT_BYTES 8 | import io.github.g00fy2.quickie.QRScannerActivity.Companion.EXTRA_RESULT_EXCEPTION 9 | import io.github.g00fy2.quickie.QRScannerActivity.Companion.EXTRA_RESULT_PARCELABLE 10 | import io.github.g00fy2.quickie.QRScannerActivity.Companion.EXTRA_RESULT_VALUE 11 | import io.github.g00fy2.quickie.content.AddressParcelable 12 | import io.github.g00fy2.quickie.content.CalendarDateTimeParcelable 13 | import io.github.g00fy2.quickie.content.CalendarEventParcelable 14 | import io.github.g00fy2.quickie.content.ContactInfoParcelable 15 | import io.github.g00fy2.quickie.content.EmailParcelable 16 | import io.github.g00fy2.quickie.content.GeoPointParcelable 17 | import io.github.g00fy2.quickie.content.PersonNameParcelable 18 | import io.github.g00fy2.quickie.content.PhoneParcelable 19 | import io.github.g00fy2.quickie.content.QRContent 20 | import io.github.g00fy2.quickie.content.QRContent.CalendarEvent 21 | import io.github.g00fy2.quickie.content.QRContent.CalendarEvent.CalendarDateTime 22 | import io.github.g00fy2.quickie.content.QRContent.ContactInfo 23 | import io.github.g00fy2.quickie.content.QRContent.ContactInfo.Address 24 | import io.github.g00fy2.quickie.content.QRContent.ContactInfo.Address.AddressType 25 | import io.github.g00fy2.quickie.content.QRContent.ContactInfo.PersonName 26 | import io.github.g00fy2.quickie.content.QRContent.Email 27 | import io.github.g00fy2.quickie.content.QRContent.Email.EmailType 28 | import io.github.g00fy2.quickie.content.QRContent.GeoPoint 29 | import io.github.g00fy2.quickie.content.QRContent.Phone 30 | import io.github.g00fy2.quickie.content.QRContent.Phone.PhoneType 31 | import io.github.g00fy2.quickie.content.QRContent.Plain 32 | import io.github.g00fy2.quickie.content.QRContent.Sms 33 | import io.github.g00fy2.quickie.content.QRContent.Url 34 | import io.github.g00fy2.quickie.content.QRContent.Wifi 35 | import io.github.g00fy2.quickie.content.SmsParcelable 36 | import io.github.g00fy2.quickie.content.UrlBookmarkParcelable 37 | import io.github.g00fy2.quickie.content.WifiParcelable 38 | 39 | internal fun Intent?.toQuickieContentType(): QRContent { 40 | val rawBytes = this?.getByteArrayExtra(EXTRA_RESULT_BYTES) 41 | val rawValue = this?.getStringExtra(EXTRA_RESULT_VALUE) 42 | return this?.toQuickieContentType(rawBytes, rawValue) ?: Plain(rawBytes, rawValue) 43 | } 44 | 45 | @Suppress("LongMethod") 46 | private fun Intent.toQuickieContentType(rawBytes: ByteArray?, rawValue: String?): QRContent? { 47 | return when (extras?.getInt(QRScannerActivity.EXTRA_RESULT_TYPE, Barcode.TYPE_UNKNOWN)) { 48 | Barcode.TYPE_CONTACT_INFO -> { 49 | IntentCompat.getParcelableExtra(this, EXTRA_RESULT_PARCELABLE, ContactInfoParcelable::class.java)?.let { 50 | ContactInfo( 51 | rawBytes = rawBytes, 52 | rawValue = rawValue, 53 | addresses = it.addressParcelables.map { address -> address.toAddress() }, 54 | emails = it.emailParcelables.map { mail -> mail.toEmail(rawBytes, rawValue) }, 55 | name = it.nameParcelable.toPersonName(), 56 | organization = it.organization, 57 | phones = it.phoneParcelables.map { phone -> phone.toPhone(rawBytes, rawValue) }, 58 | title = it.title, 59 | urls = it.urls 60 | ) 61 | } 62 | } 63 | Barcode.TYPE_EMAIL -> { 64 | IntentCompat.getParcelableExtra(this, EXTRA_RESULT_PARCELABLE, EmailParcelable::class.java)?.let { 65 | Email( 66 | rawBytes = rawBytes, 67 | rawValue = rawValue, 68 | address = it.address, 69 | body = it.body, 70 | subject = it.subject, 71 | type = EmailType.entries.getOrElse(it.type) { EmailType.UNKNOWN } 72 | ) 73 | } 74 | } 75 | Barcode.TYPE_PHONE -> { 76 | IntentCompat.getParcelableExtra(this, EXTRA_RESULT_PARCELABLE, PhoneParcelable::class.java)?.let { 77 | Phone( 78 | rawBytes = rawBytes, 79 | rawValue = rawValue, 80 | number = it.number, 81 | type = PhoneType.entries.getOrElse(it.type) { PhoneType.UNKNOWN } 82 | ) 83 | } 84 | } 85 | Barcode.TYPE_SMS -> { 86 | IntentCompat.getParcelableExtra(this, EXTRA_RESULT_PARCELABLE, SmsParcelable::class.java)?.let { 87 | Sms( 88 | rawBytes = rawBytes, 89 | rawValue = rawValue, 90 | message = it.message, 91 | phoneNumber = it.phoneNumber 92 | ) 93 | } 94 | } 95 | Barcode.TYPE_URL -> { 96 | IntentCompat.getParcelableExtra(this, EXTRA_RESULT_PARCELABLE, UrlBookmarkParcelable::class.java)?.let { 97 | Url( 98 | rawBytes = rawBytes, 99 | rawValue = rawValue, 100 | title = it.title, 101 | url = it.url 102 | ) 103 | } 104 | } 105 | Barcode.TYPE_WIFI -> { 106 | IntentCompat.getParcelableExtra(this, EXTRA_RESULT_PARCELABLE, WifiParcelable::class.java)?.let { 107 | Wifi( 108 | rawBytes = rawBytes, 109 | rawValue = rawValue, 110 | encryptionType = it.encryptionType, 111 | password = it.password, 112 | ssid = it.ssid 113 | ) 114 | } 115 | } 116 | Barcode.TYPE_GEO -> { 117 | IntentCompat.getParcelableExtra(this, EXTRA_RESULT_PARCELABLE, GeoPointParcelable::class.java)?.let { 118 | GeoPoint( 119 | rawBytes = rawBytes, 120 | rawValue = rawValue, 121 | lat = it.lat, 122 | lng = it.lng 123 | ) 124 | } 125 | } 126 | Barcode.TYPE_CALENDAR_EVENT -> { 127 | IntentCompat.getParcelableExtra(this, EXTRA_RESULT_PARCELABLE, CalendarEventParcelable::class.java)?.let { 128 | CalendarEvent( 129 | rawBytes = rawBytes, 130 | rawValue = rawValue, 131 | description = it.description, 132 | end = it.end.toCalendarEvent(), 133 | location = it.location, 134 | organizer = it.organizer, 135 | start = it.start.toCalendarEvent(), 136 | status = it.status, 137 | summary = it.summary 138 | ) 139 | } 140 | } 141 | else -> null 142 | } 143 | } 144 | 145 | internal fun Intent?.getRootException(): Exception { 146 | return this?.let { IntentCompat.getParcelableExtra(it, EXTRA_RESULT_EXCEPTION, Exception::class.java) } 147 | ?: IllegalStateException("Could retrieve root exception") 148 | } 149 | 150 | private fun PhoneParcelable.toPhone(rawBytes: ByteArray?, rawValue: String?) = 151 | Phone( 152 | rawBytes = rawBytes, 153 | rawValue = rawValue, 154 | number = number, 155 | type = PhoneType.entries.getOrElse(type) { PhoneType.UNKNOWN } 156 | ) 157 | 158 | private fun EmailParcelable.toEmail(rawBytes: ByteArray?, rawValue: String?) = 159 | Email( 160 | rawBytes = rawBytes, 161 | rawValue = rawValue, 162 | address = address, 163 | body = body, 164 | subject = subject, 165 | type = EmailType.entries.getOrElse(type) { EmailType.UNKNOWN } 166 | ) 167 | 168 | private fun AddressParcelable.toAddress() = 169 | Address( 170 | addressLines = addressLines, 171 | type = AddressType.entries.getOrElse(type) { AddressType.UNKNOWN } 172 | ) 173 | 174 | private fun PersonNameParcelable.toPersonName() = 175 | PersonName( 176 | first = first, 177 | formattedName = formattedName, 178 | last = last, 179 | middle = middle, 180 | prefix = prefix, 181 | pronunciation = pronunciation, 182 | suffix = suffix 183 | ) 184 | 185 | private fun CalendarDateTimeParcelable.toCalendarEvent() = 186 | CalendarDateTime( 187 | day = day, 188 | hours = hours, 189 | minutes = minutes, 190 | month = month, 191 | seconds = seconds, 192 | year = year, 193 | utc = utc 194 | ) -------------------------------------------------------------------------------- /quickie/src/main/kotlin/io/github/g00fy2/quickie/extensions/ScannerConfigExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie.extensions 2 | 3 | import io.github.g00fy2.quickie.config.ParcelableScannerConfig 4 | import io.github.g00fy2.quickie.config.ScannerConfig 5 | 6 | internal fun ScannerConfig.toParcelableConfig() = 7 | ParcelableScannerConfig( 8 | formats = formats, 9 | stringRes = stringRes, 10 | drawableRes = drawableRes, 11 | hapticFeedback = hapticFeedback, 12 | showTorchToggle = showTorchToggle, 13 | horizontalFrameRatio = horizontalFrameRatio, 14 | useFrontCamera = useFrontCamera, 15 | showCloseButton = showCloseButton, 16 | keepScreenOn = keepScreenOn, 17 | ) -------------------------------------------------------------------------------- /quickie/src/main/res/drawable/quickie_bg_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /quickie/src/main/res/drawable/quickie_ic_close.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 23 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/drawable/quickie_ic_qrcode.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 23 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/drawable/quickie_ic_torch.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 23 | 26 | 29 | -------------------------------------------------------------------------------- /quickie/src/main/res/layout/quickie_overlay_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 28 | 29 | 41 | 42 | 54 | 55 | 64 | 65 | 73 | 74 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /quickie/src/main/res/layout/quickie_scanner_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-af/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Skandeer QR-kode" 24 | "Wag asseblief…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-am/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR ኮድን ይቃኙ" 24 | "እባክዎ ይጠብቁ…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-ar/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "مسح رمز الاستجابة السريعة" 24 | "يرجى الانتظار…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-as/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "কিউআৰ ক’ড স্কেন কৰক" 24 | "অনুগ্রহ কৰি অপেক্ষা কৰক…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-az/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR kodunu skan edin" 24 | "Lütfən, gözləyin…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-be/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Сканіраваць QR-код" 24 | "Калі ласка, пачакайце…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-bg/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Сканиране на QR кода" 24 | "Моля, изчакайте…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-bn/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR কোড স্ক্যান করুন" 24 | "দয়া করে অপেক্ষা করুন…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-bs/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Skenirajte QR kôd" 24 | "Pričekajte…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-ca/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Escaneja un codi QR" 24 | "Espera…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-cs/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Naskenovat QR kód" 24 | "Prosím čekejte…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-da/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Scan QR-kode" 24 | "Vent et øjeblik…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR-Code scannen" 24 | "Bitte warten…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-el/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Σάρωση κωδικού QR" 24 | "Περιμένετε…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-en/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | Scan QR code 24 | Please wait… 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-es-rUS/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Escanear código QR" 24 | "Espera un momento…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Escanea el código QR" 24 | "Espera…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-et/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR-koodi skannimine" 24 | "Oodake…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-eu/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Eskaneatu QR kodea" 24 | "Itxaron, mesedez…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-fa/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "اسکن رمزینه پاسخ‌سریع" 24 | "لطفاً صبر کنید…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-fi/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Lue QR-koodi" 24 | "Odota…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-fr-rCA/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Numériser le code QR" 24 | "Veuillez patienter…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Scanner le code QR" 24 | "Veuillez patienter…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-gl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Escanear código QR" 24 | "Agarda…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-gu/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR કોડ સ્કૅન કરો" 24 | "કૃપા કરીને રાહ જુઓ…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-hi/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR कोड स्कैन करें" 24 | "कृपया प्रतीक्षा करें…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-hr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Skenirajte QR kôd" 24 | "Pričekajte…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-hu/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR-kód beolvasása" 24 | "Kérjük, várjon…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-hy/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Սկանավորեք QR կոդը" 24 | "Խնդրում ենք սպասել…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-in/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Pindai kode QR" 24 | "Harap tunggu…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-is/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Skanna QR-kóða" 24 | "Augnablik…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-it/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Scansiona codice QR" 24 | "Attendere prego…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-iw/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "‏סריקת קוד QR" 24 | "אנא המתן…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR コードのスキャン" 24 | "お待ちください…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-ka/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR კოდის სკანირება" 24 | "გთხოვთ, დაელოდოთ…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-kk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR кодын сканерлеу" 24 | "Күте тұрыңыз…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-km/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "ស្កេន​កូដ QR" 24 | "សូម​រង់ចាំ…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-kn/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR ಕೋಡ್ ಸ್ಕ್ಯಾನ್ ಮಾಡಿ" 24 | "ದಯವಿಟ್ಟು ನಿರೀಕ್ಷಿಸಿ…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-ko/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR 코드 스캔" 24 | "잠시 기다려 주세요…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-ky/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR кодун скандоо" 24 | "Күтө туруңуз…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-lo/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "ສະແກນລະຫັດ QR" 24 | "ກະລຸນາລໍຖ້າ…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-lt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR kodo nuskaitymas" 24 | "Palaukite…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-lv/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR koda skenēšana" 24 | "Lūdzu, uzgaidiet…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-mk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Скенирајте QR-код" 24 | "Почекајте…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-ml/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR കോഡ് സ്‌കാൻ ചെയ്യുക" 24 | "കാത്തിരിക്കുക…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-mn/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR кодыг скан хийх" 24 | "Түр хүлээнэ үү…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-mr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR कोड स्कॅन करा" 24 | "कृपया प्रतीक्षा करा…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-ms/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Imbas kod QR" 24 | "Sila tunggu…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-my/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR ကုဒ်ကို စကင်ဖတ်ပါ" 24 | "ခဏစောင့်ပါ…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-nb/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Skann QR-koden" 24 | "Vent litt…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-ne/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR कोड स्क्यान गर्नुहोस्" 24 | "कृपया प्रतीक्षा गर्नुहोला…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-nl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR-code scannen" 24 | "Even geduld…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-or/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR କୋଡ୍‍ ସ୍କାନ୍‍ କରନ୍ତୁ" 24 | "ଦୟାକରି ଅପେକ୍ଷା କରନ୍ତୁ…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-pa/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR ਕੋਡ ਸਕੈਨ ਕਰੋ" 24 | "ਕਿਰਪਾ ਕਰਕੇ ਠਹਿਰੋ…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-pl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Zeskanuj kod QR" 24 | "Zaczekaj…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-pt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Ler código QR" 24 | "Aguarde…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-ro/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Scanați codul QR" 24 | "Așteptați…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-ru/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Сканируйте QR-код" 24 | "Подождите…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-si/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR කේතය ස්කෑන් කරන්න" 24 | "කරුණාකර රැඳී සිටින්න…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-sk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Skenovanie QR kódu" 24 | "Čakajte…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-sl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Branje kode QR" 24 | "Počakajte…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-sq/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Skano kodin QR" 24 | "Qëndro në pritje…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-sr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Скенирај QR кôд" 24 | "Сачекајте…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-sv/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Skanna QR-kod" 24 | "Vänta…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-sw/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Changanua msimbo wa QR" 24 | "Tafadhali subiri…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-ta/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR குறியீட்டை ஸ்கேன் செய்தல்" 24 | "காத்திருக்கவும்…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-te/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR కోడ్‌ని స్కాన్ చేయండి" 24 | "దయచేసి వేచి ఉండండి…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-th/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "สแกนคิวอาร์โค้ด" 24 | "โปรดรอสักครู่…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-tl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "I-scan ang QR code" 24 | "Mangyaring maghintay…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-tr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR kodu tarayın" 24 | "Lütfen bekleyin…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-uk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Відскануйте QR-код" 24 | "Зачекайте…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-ur/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "‏QR کوڈ اسکین کریں" 24 | "براہ کرم انتظار کریں…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-uz/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "QR kodni skanerlash" 24 | "Iltimos, kuting…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-v23/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-v27/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-v29/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-vi/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Quét mã QR" 24 | "Vui lòng chờ…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "扫描二维码" 24 | "请稍候…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-zh-rHK/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "掃瞄二維條碼" 24 | "請稍候…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "掃描 QR 圖碼" 24 | "請稍候…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values-zu/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | "Skena ikhodi ye-QR" 24 | "Sicela ulinde…" 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #dadce0 4 | #ffffff 5 | #00000000 6 | #03dac5 7 | -------------------------------------------------------------------------------- /quickie/src/main/res/values/public.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /quickie/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | Scan QR code 24 | Please wait… 25 | 26 | -------------------------------------------------------------------------------- /quickie/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /quickie/src/test/kotlin/io/github/g00fy2/quickie/BarcodeFormatsTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie 2 | 3 | import com.google.mlkit.vision.barcode.common.Barcode 4 | import io.github.g00fy2.quickie.config.BarcodeFormat 5 | import org.junit.jupiter.api.Assertions.assertEquals 6 | import org.junit.jupiter.api.Test 7 | 8 | internal class BarcodeFormatsTest { 9 | 10 | @Test 11 | fun `Ml kit barcode formats are fully mapped`() { 12 | val mlKitBarcodeFormats: Map = Barcode::class.java.declaredFields 13 | .filter { it.type == Int::class.java } 14 | .filter { it.name.startsWith("FORMAT_") } 15 | .filter { it.name != "FORMAT_UNKNOWN" } 16 | .associate { it.name to it.getInt(null) } 17 | 18 | val quickieBarcodeFormats: Map = BarcodeFormat.entries.associate { it.name to it.value } 19 | 20 | assertEquals(mlKitBarcodeFormats, quickieBarcodeFormats) 21 | } 22 | } -------------------------------------------------------------------------------- /quickie/src/unbundled/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /quickie/src/unbundled/kotlin/io/github/g00fy2/quickie/utils/MlKitErrorHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickie.utils 2 | 3 | import com.google.android.gms.common.ConnectionResult 4 | import com.google.android.gms.common.GoogleApiAvailability 5 | import com.google.mlkit.common.MlKitException 6 | import io.github.g00fy2.quickie.QRScannerActivity 7 | 8 | internal object MlKitErrorHandler { 9 | 10 | // version 20.12.14 (as suggested https://github.com/firebase/firebase-android-sdk/issues/407#issuecomment-632288258) 11 | private const val MIN_SERVICES_VERSION = 201214 * 1000 12 | private const val REQUEST_CODE = 9000 13 | 14 | internal fun isResolvableError(activity: QRScannerActivity, exception: Exception): Boolean { 15 | if (exception is MlKitException && exception.errorCode == MlKitException.UNAVAILABLE) { 16 | // check if Google Play services is available and its version is at least MIN_SERVICES_VERSION 17 | val gmsCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(activity, MIN_SERVICES_VERSION) 18 | 19 | if (activity.errorDialog?.isShowing != true && gmsCode != ConnectionResult.SUCCESS && 20 | GoogleApiAvailability.getInstance().isUserResolvableError(gmsCode) 21 | ) { 22 | activity.errorDialog = GoogleApiAvailability.getInstance().getErrorDialog(activity, gmsCode, REQUEST_CODE) 23 | } 24 | return true 25 | } 26 | return false 27 | } 28 | } -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | } 5 | 6 | android { 7 | namespace = "io.github.g00fy2.quickiesample" 8 | defaultConfig { 9 | applicationId = "io.github.g00fy2.quickiesample" 10 | versionCode = 1 11 | versionName = "1.0" 12 | } 13 | buildTypes { 14 | getByName("release") { 15 | isShrinkResources = true 16 | isMinifyEnabled = true 17 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 18 | } 19 | } 20 | splits { 21 | abi { 22 | isEnable = true 23 | reset() 24 | include("x86", "armeabi-v7a", "arm64-v8a", "x86_64") 25 | isUniversalApk = true 26 | } 27 | } 28 | flavorDimensions += "mlkit" 29 | productFlavors { 30 | create("bundled").dimension = "mlkit" 31 | create("unbundled").dimension = "mlkit" 32 | } 33 | buildFeatures { 34 | buildConfig = true 35 | viewBinding = true 36 | } 37 | lint { 38 | abortOnError = true 39 | warningsAsErrors = true 40 | checkDependencies = true 41 | disable.addAll( 42 | listOf( 43 | "RtlEnabled", 44 | "GradleDependency", 45 | "AndroidGradlePluginVersion", 46 | "OldTargetApi", 47 | ) 48 | ) 49 | } 50 | } 51 | 52 | dependencies { 53 | implementation(project(":quickie")) 54 | 55 | implementation(libs.google.materialDesign) 56 | } -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -renamesourcefileattribute SourceFile 2 | -keepattributes SourceFile,LineNumberTable -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/io/github/g00fy2/quickiesample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickiesample 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.widget.ArrayAdapter 7 | import android.widget.TextView 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.core.net.toUri 10 | import com.google.android.material.snackbar.Snackbar 11 | import io.github.g00fy2.quickie.QRResult 12 | import io.github.g00fy2.quickie.QRResult.QRError 13 | import io.github.g00fy2.quickie.QRResult.QRMissingPermission 14 | import io.github.g00fy2.quickie.QRResult.QRSuccess 15 | import io.github.g00fy2.quickie.QRResult.QRUserCanceled 16 | import io.github.g00fy2.quickie.ScanCustomCode 17 | import io.github.g00fy2.quickie.ScanQRCode 18 | import io.github.g00fy2.quickie.config.BarcodeFormat 19 | import io.github.g00fy2.quickie.config.ScannerConfig 20 | import io.github.g00fy2.quickie.content.QRContent 21 | import io.github.g00fy2.quickiesample.databinding.ActivityMainBinding 22 | 23 | class MainActivity : AppCompatActivity() { 24 | 25 | private lateinit var binding: ActivityMainBinding 26 | private var selectedBarcodeFormat = BarcodeFormat.FORMAT_ALL_FORMATS 27 | 28 | private val scanQrCode = registerForActivityResult(ScanQRCode(), ::showSnackbar) 29 | private val scanCustomCode = registerForActivityResult(ScanCustomCode(), ::showSnackbar) 30 | 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | binding = ActivityMainBinding.inflate(layoutInflater) 34 | setContentView(binding.root) 35 | setBarcodeFormatDropdown() 36 | 37 | binding.qrScannerButton.setOnClickListener { 38 | scanQrCode.launch(null) 39 | } 40 | 41 | binding.customScannerButton.setOnClickListener { 42 | scanCustomCode.launch( 43 | ScannerConfig.build { 44 | setBarcodeFormats(listOf(selectedBarcodeFormat)) // set interested barcode formats 45 | setOverlayStringRes(R.string.scan_barcode) // string resource used for the scanner overlay 46 | setOverlayDrawableRes(R.drawable.ic_scan_barcode) // drawable resource used for the scanner overlay 47 | setHapticSuccessFeedback(false) // enable (default) or disable haptic feedback when a barcode was detected 48 | setShowTorchToggle(true) // show or hide (default) torch/flashlight toggle button 49 | setShowCloseButton(true) // show or hide (default) close button 50 | setHorizontalFrameRatio(2.2f) // set the horizontal overlay ratio (default is 1 / square frame) 51 | setUseFrontCamera(false) // use the front camera 52 | setKeepScreenOn(true) // keep the device's screen turned on 53 | } 54 | ) 55 | } 56 | 57 | if (intent.extras?.getBoolean(OPEN_SCANNER) == true) scanQrCode.launch(null) 58 | } 59 | 60 | private fun showSnackbar(result: QRResult) { 61 | val text = when (result) { 62 | is QRSuccess -> { 63 | result.content.rawValue 64 | // decoding with default UTF-8 charset when rawValue is null will not result in meaningful output, demo purpose 65 | ?: result.content.rawBytes?.let { String(it) }.orEmpty() 66 | } 67 | QRUserCanceled -> "User canceled" 68 | QRMissingPermission -> "Missing permission" 69 | is QRError -> "${result.exception.javaClass.simpleName}: ${result.exception.localizedMessage}" 70 | } 71 | 72 | Snackbar.make(binding.root, text, Snackbar.LENGTH_INDEFINITE).apply { 73 | view.findViewById(com.google.android.material.R.id.snackbar_text)?.run { 74 | maxLines = 5 75 | setTextIsSelectable(true) 76 | } 77 | if (result is QRSuccess) { 78 | val content = result.content 79 | if (content is QRContent.Url) { 80 | setAction(R.string.open_action) { openUrl(content.url) } 81 | return@apply 82 | } 83 | } 84 | setAction(R.string.ok_action) { } 85 | }.show() 86 | } 87 | 88 | private fun openUrl(url: String) { 89 | try { 90 | startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) 91 | } catch (ignored: ActivityNotFoundException) { 92 | // no Activity found to run the given Intent 93 | } 94 | } 95 | 96 | private fun setBarcodeFormatDropdown() { 97 | ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, BarcodeFormat.entries.map { it.name }).let { 98 | binding.barcodeFormatsAutoCompleteTextView.setAdapter(it) 99 | binding.barcodeFormatsAutoCompleteTextView.setText(it.getItem(it.getPosition(selectedBarcodeFormat.name)), false) 100 | } 101 | binding.barcodeFormatsAutoCompleteTextView.setOnItemClickListener { _, _, position, _ -> 102 | selectedBarcodeFormat = BarcodeFormat.entries[position] 103 | } 104 | } 105 | 106 | companion object { 107 | const val OPEN_SCANNER = "open_scanner" 108 | } 109 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/io/github/g00fy2/quickiesample/SampleApp.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickiesample 2 | 3 | import android.app.Application 4 | import android.os.StrictMode 5 | 6 | class SampleApp : Application() { 7 | 8 | override fun onCreate() { 9 | if (BuildConfig.DEBUG) { 10 | StrictMode.setThreadPolicy( 11 | StrictMode.ThreadPolicy.Builder() 12 | .detectAll() 13 | .penaltyLog() 14 | .build() 15 | ) 16 | StrictMode.setVmPolicy( 17 | StrictMode.VmPolicy.Builder() 18 | .detectAll() 19 | .penaltyLog() 20 | .build() 21 | ) 22 | } 23 | 24 | super.onCreate() 25 | } 26 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/io/github/g00fy2/quickiesample/quicksettingstile/QuickieTileService.kt: -------------------------------------------------------------------------------- 1 | package io.github.g00fy2.quickiesample.quicksettingstile 2 | 3 | import android.app.PendingIntent 4 | import android.content.ComponentName 5 | import android.content.Intent 6 | import android.os.Build 7 | import android.os.IBinder 8 | import android.service.quicksettings.Tile 9 | import android.service.quicksettings.TileService 10 | import androidx.annotation.RequiresApi 11 | import androidx.core.service.quicksettings.PendingIntentActivityWrapper 12 | import androidx.core.service.quicksettings.TileServiceCompat 13 | import io.github.g00fy2.quickiesample.MainActivity 14 | 15 | // optional service to allow launching the sample app from the quick settings 16 | @RequiresApi(Build.VERSION_CODES.N) 17 | class QuickieTileService : TileService() { 18 | 19 | override fun onBind(intent: Intent?): IBinder? { 20 | requestListeningState(this, ComponentName(this, QuickieTileService::class.java)) 21 | return super.onBind(intent) 22 | } 23 | 24 | override fun onStartListening() { 25 | super.onStartListening() 26 | qsTile?.run { 27 | state = Tile.STATE_INACTIVE 28 | updateTile() 29 | } 30 | } 31 | 32 | override fun onClick() { 33 | super.onClick() 34 | 35 | val intent = Intent(this, MainActivity::class.java).apply { 36 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) 37 | putExtra(MainActivity.OPEN_SCANNER, true) 38 | } 39 | 40 | TileServiceCompat.startActivityAndCollapse( 41 | this, 42 | PendingIntentActivityWrapper(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT, true) 43 | ) 44 | } 45 | } -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 35 | 38 | 41 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_qs_qrcode.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 23 | 26 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_scan_barcode.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 |