├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── font
│ │ │ │ ├── roboto_medium.ttf
│ │ │ │ ├── roboto_medium_italic.ttf
│ │ │ │ ├── roboto_bold.ttf
│ │ │ │ ├── roboto_italic.ttf
│ │ │ │ ├── roboto_regular.ttf
│ │ │ │ ├── roboto_bold_italic.ttf
│ │ │ │ ├── bold.xml
│ │ │ │ ├── regular.xml
│ │ │ │ └── medium.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── drawable
│ │ │ │ ├── toggle_text_color.xml
│ │ │ │ ├── toggle_background_border.xml
│ │ │ │ ├── ic_person.xml
│ │ │ │ ├── ic_check_circle_outline.xml
│ │ │ │ ├── ic_close_circle_outline.xml
│ │ │ │ ├── ic_help_circle_outline.xml
│ │ │ │ ├── toggle_background_left.xml
│ │ │ │ ├── toggle_background_right.xml
│ │ │ │ ├── ic_passport.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ ├── fragment_photo.xml
│ │ │ │ ├── activity_nfc.xml
│ │ │ │ ├── activity_camera.xml
│ │ │ │ ├── activity_photo.xml
│ │ │ │ ├── fragment_camera_mrz.xml
│ │ │ │ ├── fragment_nfc.xml
│ │ │ │ └── fragment_selection.xml
│ │ │ ├── values
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── styles.xml
│ │ │ │ └── strings.xml
│ │ │ └── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ ├── assets
│ │ │ └── tessdata
│ │ │ │ ├── eng.traineddata
│ │ │ │ └── eng.user-patterns
│ │ ├── java
│ │ │ ├── example
│ │ │ │ └── jllarraz
│ │ │ │ │ └── com
│ │ │ │ │ └── passportreader
│ │ │ │ │ ├── common
│ │ │ │ │ ├── IntentData.kt
│ │ │ │ │ └── PreferencesKeys.kt
│ │ │ │ │ ├── network
│ │ │ │ │ ├── MasterListApi.kt
│ │ │ │ │ └── MasterListService.kt
│ │ │ │ │ ├── utils
│ │ │ │ │ ├── EACCredentials.kt
│ │ │ │ │ ├── StringUtils.kt
│ │ │ │ │ ├── MRZUtil.kt
│ │ │ │ │ ├── ImageUtil.kt
│ │ │ │ │ ├── KeyStoreUtils.kt
│ │ │ │ │ ├── OcrUtils.kt
│ │ │ │ │ └── PassportNfcUtils.kt
│ │ │ │ │ ├── ui
│ │ │ │ │ ├── validators
│ │ │ │ │ │ ├── DateRule.kt
│ │ │ │ │ │ └── DocumentNumberRule.kt
│ │ │ │ │ ├── activities
│ │ │ │ │ │ ├── CameraActivity.kt
│ │ │ │ │ │ ├── SelectionActivity.kt
│ │ │ │ │ │ └── NfcActivity.kt
│ │ │ │ │ └── fragments
│ │ │ │ │ │ ├── PassportPhotoFragment.kt
│ │ │ │ │ │ └── NfcFragment.kt
│ │ │ │ │ ├── mlkit
│ │ │ │ │ ├── FrameMetadata.kt
│ │ │ │ │ ├── OcrMrzDetectorProcessor.kt
│ │ │ │ │ ├── VisionImageProcessor.kt
│ │ │ │ │ └── GraphicOverlay.kt
│ │ │ │ │ └── data
│ │ │ │ │ ├── PersonDetails.kt
│ │ │ │ │ ├── AdditionalDocumentDetails.kt
│ │ │ │ │ ├── Passport.kt
│ │ │ │ │ └── AdditionalPersonDetails.kt
│ │ │ └── org
│ │ │ │ └── jmrtd
│ │ │ │ ├── cert
│ │ │ │ ├── PKDMasterListCertStoreParameters.kt
│ │ │ │ ├── KeyStoreCertStoreSpi.kt
│ │ │ │ ├── PKDCertStoreParameters.kt
│ │ │ │ ├── KeyStoreCertStoreParameters.kt
│ │ │ │ └── CSCAMasterList.kt
│ │ │ │ └── FeatureStatus.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── example
│ │ │ └── jllarraz
│ │ │ └── com
│ │ │ └── passportreader
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── example
│ │ └── jllarraz
│ │ └── com
│ │ └── passportreader
│ │ └── ExampleInstrumentedTest.java
├── libs
│ └── jj2000_imageutil.jar
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── examples
└── passport_ireland.jpg
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── caches
│ └── build_file_checksums.ser
├── encodings.xml
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── compiler.xml
├── vcs.xml
├── gradle.xml
├── jarRepositories.xml
└── misc.xml
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── README.md
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_medium.ttf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_medium_italic.ttf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/libs/jj2000_imageutil.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/libs/jj2000_imageutil.jar
--------------------------------------------------------------------------------
/examples/passport_ireland.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/examples/passport_ireland.jpg
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.idea/caches/build_file_checksums.ser:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/.idea/caches/build_file_checksums.ser
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/res/font/roboto_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/res/font/roboto_italic.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/res/font/roboto_regular.ttf
--------------------------------------------------------------------------------
/app/src/main/assets/tessdata/eng.traineddata:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/assets/tessdata/eng.traineddata
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_bold_italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/res/font/roboto_bold_italic.ttf
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jllarraz/AndroidPassportReader/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/libraries
5 | /.idea/modules.xml
6 | /.idea/workspace.xml
7 | .DS_Store
8 | /build
9 | /captures
10 | .externalNativeBuild
11 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/assets/tessdata/eng.user-patterns:
--------------------------------------------------------------------------------
1 | ^P<[
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/toggle_background_border.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_person.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/network/MasterListApi.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.network
2 |
3 | import io.reactivex.Single
4 | import okhttp3.ResponseBody
5 | import retrofit2.http.*
6 |
7 | interface MasterListApi {
8 | @Headers(value = ["Content-type: text/xml; charset=utf-8"])
9 | @GET("descargas/mrtd/SpanishMasterList.zip")
10 | @Streaming
11 | fun getSpanishMasterList(
12 | ): Single
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/utils/EACCredentials.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.utils
2 |
3 | import java.security.PrivateKey
4 | import java.security.cert.Certificate
5 |
6 | /**
7 | * Encapsulates the terminal key and associated certificate chain for terminal authentication.
8 | */
9 | class EACCredentials
10 | /**
11 | * Creates EAC credentials.
12 | *
13 | * @param privateKey
14 | * @param chain
15 | */
16 | (val privateKey: PrivateKey, val chain: Array)
17 |
--------------------------------------------------------------------------------
/app/src/test/java/example/jllarraz/com/passportreader/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_check_circle_outline.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/utils/StringUtils.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.utils
2 |
3 | object StringUtils {
4 | private val hexArray = "0123456789ABCDEF".toCharArray()
5 | fun bytesToHex(bytes: ByteArray): String {
6 | val hexChars = CharArray(bytes.size * 2)
7 | for (j in bytes.indices) {
8 | val v = bytes[j].toInt() and 0xFF
9 | hexChars[j * 2] = hexArray[v.ushr(4)]
10 | hexChars[j * 2 + 1] = hexArray[v and 0x0F]
11 | }
12 | return String(hexChars)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_close_circle_outline.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_help_circle_outline.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/toggle_background_left.xml:
--------------------------------------------------------------------------------
1 |
2 | -
3 |
4 |
5 |
6 |
7 |
8 |
9 | -
10 |
11 |
12 |
13 |
14 | -
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/toggle_background_right.xml:
--------------------------------------------------------------------------------
1 |
2 | -
3 |
4 |
5 |
6 |
7 |
8 |
9 | -
10 |
11 |
12 |
13 |
14 | -
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/font/bold.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
11 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/font/regular.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
11 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/font/medium.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
11 |
18 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | android.enableJetifier=true
10 | android.useAndroidX=true
11 | org.gradle.jvmargs=-Xmx1536m
12 | # When configured, Gradle will run in incubating parallel mode.
13 | # This option should only be used with decoupled projects. More details, visit
14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
15 | # org.gradle.parallel=true
16 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_photo.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
20 |
21 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/example/jllarraz/com/passportreader/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader;
2 |
3 | import android.content.Context;
4 | import androidx.test.InstrumentationRegistry;
5 | import androidx.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("example.jllarraz.com.passportreader", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_nfc.xml:
--------------------------------------------------------------------------------
1 |
16 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_camera.xml:
--------------------------------------------------------------------------------
1 |
16 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_photo.xml:
--------------------------------------------------------------------------------
1 |
16 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 10dp
5 | 36dp
6 | 4dp
7 | 8dp
8 | 12dp
9 | 22dp
10 | 16sp
11 | 13sp
12 | 14sp
13 |
14 | 0dp
15 | 112dp
16 | 112dp
17 |
18 | 48dp
19 | 12dp
20 | 1dp
21 |
22 | 4dp
23 | 8dp
24 | 4dp
25 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/ui/validators/DateRule.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.ui.validators
2 |
3 | import android.content.Context
4 | import androidx.appcompat.widget.AppCompatEditText
5 | import android.widget.EditText
6 |
7 | import com.mobsandgeeks.saripaar.QuickRule
8 |
9 | import java.util.regex.Matcher
10 | import java.util.regex.Pattern
11 |
12 | import example.jllarraz.com.passportreader.R
13 |
14 |
15 | /**
16 | * Created by Surface on 15/08/2017.
17 | */
18 |
19 | class DateRule : QuickRule() {
20 |
21 | override fun isValid(editText: AppCompatEditText): Boolean {
22 | val text = editText.text!!.toString().trim { it <= ' ' }
23 | val patternDate = Pattern.compile(REGEX)
24 | val matcherDate = patternDate.matcher(text)
25 | return matcherDate.find()
26 | }
27 |
28 | override fun getMessage(context: Context): String {
29 | return context.getString(R.string.error_validation_date)
30 | }
31 |
32 | companion object {
33 |
34 | private val REGEX = "[0-9]{6}$"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 | #ffffff
8 | #D9000000
9 | #616365
10 |
11 | #000000
12 | #ffffff
13 | #413392
14 | #1f000000
15 | #e6e8ee
16 | #2A3764
17 |
18 | #ffffffff
19 | #ffd6d6d6
20 | #60000000
21 | #ffffffff
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/ui/validators/DocumentNumberRule.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.ui.validators
2 |
3 | import android.content.Context
4 | import androidx.appcompat.widget.AppCompatEditText
5 | import android.widget.EditText
6 |
7 | import com.mobsandgeeks.saripaar.QuickRule
8 |
9 | import java.util.regex.Matcher
10 | import java.util.regex.Pattern
11 |
12 | import example.jllarraz.com.passportreader.R
13 |
14 |
15 | /**
16 | * Created by Surface on 15/08/2017.
17 | */
18 |
19 | class DocumentNumberRule : QuickRule() {
20 |
21 | override fun isValid(editText: AppCompatEditText): Boolean {
22 | val text = editText.text!!.toString().trim { it <= ' ' }
23 | val patternDate = Pattern.compile(REGEX)
24 | val matcherDate = patternDate.matcher(text)
25 | return matcherDate.find()
26 | }
27 |
28 | override fun getMessage(context: Context): String {
29 | return context.getString(R.string.error_validation_document_number)
30 | }
31 |
32 | companion object {
33 |
34 | private val REGEX = "[A-Z0-9<]{9}$"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_passport.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/java/org/jmrtd/cert/PKDMasterListCertStoreParameters.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * JMRTD - A Java API for accessing machine readable travel documents.
3 | *
4 | * Copyright (C) 2006 - 2013 The JMRTD team
5 | *
6 | * This library is free software; you can redistribute it and/or
7 | * modify it under the terms of the GNU Lesser General Public
8 | * License as published by the Free Software Foundation; either
9 | * version 2.1 of the License, or (at your option) any later version.
10 | *
11 | * This library is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 | * Lesser General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Lesser General Public
17 | * License along with this library; if not, write to the Free Software
18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 | *
20 | * $Id: $
21 | */
22 |
23 | package org.jmrtd.cert
24 |
25 | /**
26 | * Parameters for PKD backed certificate store, selecting certificates provided
27 | * in CSCA master lists.
28 | *
29 | * @author The JMRTD team (info@jmrtd.org)
30 | *
31 | * @version $Revision: $
32 | */
33 | class PKDMasterListCertStoreParameters : PKDCertStoreParameters {
34 |
35 | constructor() : super()
36 |
37 | @JvmOverloads
38 | constructor(serverName: String, baseDN: String = DEFAULT_BASE_DN) : super(serverName, baseDN)
39 |
40 | @JvmOverloads
41 | constructor(serverName: String, port: Int, baseDN: String = DEFAULT_BASE_DN) : super(serverName, port, baseDN)
42 |
43 | companion object {
44 |
45 | private val DEFAULT_BASE_DN = "dc=CSCAMasterList,dc=pkdDownload"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/mlkit/FrameMetadata.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package example.jllarraz.com.passportreader.mlkit
15 |
16 | /** Describing a frame info. */
17 | class FrameMetadata private constructor(val width: Int, val height: Int, val rotation: Int, val cameraFacing: Int) {
18 |
19 | /** Builder of [FrameMetadata]. */
20 | class Builder {
21 |
22 | private var width: Int = 0
23 | private var height: Int = 0
24 | private var rotation: Int = 0
25 | private var cameraFacing: Int = 0
26 |
27 | fun setWidth(width: Int): Builder {
28 | this.width = width
29 | return this
30 | }
31 |
32 | fun setHeight(height: Int): Builder {
33 | this.height = height
34 | return this
35 | }
36 |
37 | fun setRotation(rotation: Int): Builder {
38 | this.rotation = rotation
39 | return this
40 | }
41 |
42 | fun setCameraFacing(facing: Int): Builder {
43 | cameraFacing = facing
44 | return this
45 | }
46 |
47 | fun build(): FrameMetadata {
48 | return FrameMetadata(width, height, rotation, cameraFacing)
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/mlkit/OcrMrzDetectorProcessor.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package example.jllarraz.com.passportreader.mlkit
17 |
18 | import android.util.Log
19 |
20 | import com.google.android.gms.tasks.Task
21 | import com.google.mlkit.vision.common.InputImage
22 | import com.google.mlkit.vision.text.Text
23 | import com.google.mlkit.vision.text.TextRecognition
24 | import com.google.mlkit.vision.text.TextRecognizer
25 | import com.google.mlkit.vision.text.latin.TextRecognizerOptions
26 |
27 | import java.io.IOException
28 |
29 |
30 | /**
31 | * A very simple Processor which receives detected TextBlocks and adds them to the overlay
32 | * as OcrGraphics.
33 | */
34 | class OcrMrzDetectorProcessor() : VisionProcessorBase() {
35 |
36 | private val detector: TextRecognizer
37 |
38 | init {
39 | detector = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
40 |
41 | }
42 | override fun stop() {
43 | try {
44 | detector.close()
45 | } catch (e: IOException) {
46 | Log.e(TAG, "Exception thrown while trying to close Text Detector: $e")
47 | }
48 |
49 | }
50 |
51 | override fun detectInImage(image: InputImage): Task {
52 | return detector.process(image)
53 | }
54 |
55 | companion object {
56 | private val TAG = OcrMrzDetectorProcessor::class.java.simpleName
57 |
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_camera_mrz.xml:
--------------------------------------------------------------------------------
1 |
16 |
21 |
22 |
26 |
27 |
38 |
39 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/ui/activities/CameraActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package example.jllarraz.com.passportreader.ui.activities
18 |
19 | import android.app.Activity
20 | import android.content.Intent
21 | import android.os.Bundle
22 | import androidx.appcompat.app.AppCompatActivity
23 |
24 | import org.jmrtd.lds.icao.MRZInfo
25 |
26 | import example.jllarraz.com.passportreader.R
27 | import example.jllarraz.com.passportreader.common.IntentData
28 | import example.jllarraz.com.passportreader.ui.fragments.CameraMLKitFragment
29 |
30 | class CameraActivity : AppCompatActivity(), CameraMLKitFragment.CameraMLKitCallback {
31 |
32 | override fun onCreate(savedInstanceState: Bundle?) {
33 | super.onCreate(savedInstanceState)
34 | setContentView(R.layout.activity_camera)
35 | supportFragmentManager.beginTransaction()
36 | .replace(R.id.container, CameraMLKitFragment())
37 | .commit()
38 | }
39 |
40 | override fun onBackPressed() {
41 | setResult(Activity.RESULT_CANCELED)
42 | finish()
43 | }
44 |
45 | override fun onPassportRead(mrzInfo: MRZInfo) {
46 | val intent = Intent()
47 | intent.putExtra(IntentData.KEY_MRZ_INFO, mrzInfo)
48 | setResult(Activity.RESULT_OK, intent)
49 | finish()
50 | }
51 |
52 | override fun onError() {
53 | onBackPressed()
54 | }
55 |
56 | companion object {
57 |
58 | private val TAG = CameraActivity::class.java.simpleName
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
23 |
26 |
27 |
32 |
33 |
34 |
35 |
38 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/mlkit/VisionImageProcessor.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package example.jllarraz.com.passportreader.mlkit
15 |
16 | import android.graphics.Bitmap
17 | import android.media.Image
18 |
19 | import com.google.firebase.ml.common.FirebaseMLException
20 | import com.google.mlkit.vision.common.InputImage
21 | import io.fotoapparat.preview.Frame
22 |
23 | import java.nio.ByteBuffer
24 |
25 | /** An inferface to process the images with different ML Kit detectors and custom image models. */
26 | interface VisionImageProcessor {
27 |
28 | /** Processes the images with the underlying machine learning models. */
29 | @Throws(FirebaseMLException::class)
30 | fun process(data: ByteBuffer, frameMetadata: FrameMetadata, graphicOverlay: GraphicOverlay?=null, isOriginalImageReturned:Boolean = true, listener: VisionProcessorBase.Listener):Boolean
31 |
32 | /** Processes the bitmap images. */
33 | fun process(bitmap: Bitmap, rotation: Int = 0, graphicOverlay: GraphicOverlay?=null, isOriginalImageReturned:Boolean = true, convertToNv21:Boolean = true, listener: VisionProcessorBase.Listener):Boolean
34 |
35 | /** Processes the images. */
36 | fun process(image: Image, rotation: Int = 0, graphicOverlay: GraphicOverlay?=null, isOriginalImageReturned:Boolean = true, listener: VisionProcessorBase.Listener):Boolean
37 |
38 | /** Processes the bitmap images. */
39 | fun process(frame: Frame, rotation:Int = 0, graphicOverlay: GraphicOverlay?=null, isOriginalImageReturned:Boolean = true, listener: VisionProcessorBase.Listener):Boolean
40 |
41 | /** Processes the FirebaseVisionImage */
42 | fun process(image: InputImage, metadata: FrameMetadata?, graphicOverlay: GraphicOverlay?, isOriginalImageReturned:Boolean = true, listener: VisionProcessorBase.Listener):Boolean
43 |
44 | /** Stops the underlying machine learning model and release resources. */
45 | fun stop()
46 |
47 | fun canHandleNewFrame():Boolean
48 |
49 | fun resetThrottle()
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/ui/fragments/PassportPhotoFragment.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.ui.fragments
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.os.Bundle
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 |
10 | import example.jllarraz.com.passportreader.common.IntentData
11 | import example.jllarraz.com.passportreader.databinding.FragmentPhotoBinding
12 |
13 | class PassportPhotoFragment : androidx.fragment.app.Fragment() {
14 |
15 | private var passportPhotoFragmentListener: PassportPhotoFragmentListener? = null
16 |
17 | private var bitmap: Bitmap? = null
18 |
19 |
20 | private var binding:FragmentPhotoBinding?=null
21 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
22 | savedInstanceState: Bundle?): View? {
23 | binding = FragmentPhotoBinding.inflate(inflater, container, false)
24 | return binding?.root
25 | }
26 |
27 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
28 | super.onViewCreated(view, savedInstanceState)
29 |
30 | val arguments = arguments
31 | if (arguments!!.containsKey(IntentData.KEY_IMAGE)) {
32 | bitmap = arguments.getParcelable(IntentData.KEY_IMAGE)
33 | } else {
34 | //error
35 | }
36 | }
37 |
38 | override fun onResume() {
39 | super.onResume()
40 | refreshData(bitmap)
41 | }
42 |
43 | private fun refreshData(bitmap: Bitmap?) {
44 | if (bitmap == null) {
45 | return
46 | }
47 | binding?.image?.setImageBitmap(bitmap)
48 | }
49 |
50 |
51 | override fun onAttach(context: Context) {
52 | super.onAttach(context)
53 | val activity = activity
54 | if (activity is PassportPhotoFragmentListener) {
55 | passportPhotoFragmentListener = activity
56 | }
57 | }
58 |
59 | override fun onDetach() {
60 | passportPhotoFragmentListener = null
61 | super.onDetach()
62 |
63 | }
64 |
65 | override fun onDestroyView() {
66 | binding = null
67 | super.onDestroyView()
68 | }
69 |
70 | interface PassportPhotoFragmentListener
71 |
72 | companion object {
73 |
74 | fun newInstance(bitmap: Bitmap): PassportPhotoFragment {
75 | val myFragment = PassportPhotoFragment()
76 | val args = Bundle()
77 | args.putParcelable(IntentData.KEY_IMAGE, bitmap)
78 | myFragment.arguments = args
79 | return myFragment
80 | }
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Passport Reader
2 |
3 | Sample project to read Passports using MRZ or manual entry. Currently I am using ML KIT for the OCR.
4 |
5 | I don't read the the whole MRZ as ML KIT for now it's unable to read it (it's struggling with "<<<"), but I use it to read the second line and after that use a regular expression to match the rigth format.
6 |
7 | You can use the example images stored under `examples` to test the application or download any sample passport document from https://www.consilium.europa.eu/prado/EN/prado-start-page.html
8 |
9 | 
10 |
11 |
12 | This project is based in the information and tutorials found in
13 |
14 | - https://developer.android.com/reference/android/hardware/camera2/package-summary
15 | - https://github.com/tananaev/passport-reader/blob/master/app/build.gradle
16 | - https://techblog.bozho.net/how-to-read-your-passport-with-android/
17 | - https://github.com/mercuriete/android-mrz-reader
18 | - https://en.wikipedia.org/wiki/Machine-readable_passport
19 | - https://jmrtd.org/about.shtml
20 | - https://firebase.google.com/docs/ml-kit/recognize-text
21 | - https://github.com/tananaev/passport-reader
22 |
23 |
24 | ## Build & Run
25 |
26 | ```
27 | 1. Clone Repository
28 | 2. Open with Android Studio
29 | 3. Configure Android SDK
30 | 4. Launch application
31 | ```
32 |
33 | ## OCR
34 |
35 | You must put your phone horizontal when you try to read the passports MRZ.
36 |
37 | This is are examples of how the app performs.
38 | https://youtu.be/ZmRl_-3RH2U (Full read)
39 | https://youtu.be/kuIkZ1ZktCk (Just OCR)
40 |
41 | ## Country Signing Certificate Authority
42 |
43 | For the CSCA certificates the example points to the Master List provided by the spanish government. You should point it to whatever list your country has.
44 | -https://www.dnielectronico.es/PortalDNIe/PRF1_Cons02.action?pag=REF_1093&id_menu=55
45 |
46 | You can find some information in
47 | -https://jmrtd.org/certificates.shtml
48 |
49 | ## License
50 |
51 | Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
52 |
53 | http://www.apache.org/licenses/LICENSE-2.0
54 |
55 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/java/org/jmrtd/cert/KeyStoreCertStoreSpi.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * JMRTD - A Java API for accessing machine readable travel documents.
3 | *
4 | * Copyright (C) 2006 - 2013 The JMRTD team
5 | *
6 | * This library is free software; you can redistribute it and/or
7 | * modify it under the terms of the GNU Lesser General Public
8 | * License as published by the Free Software Foundation; either
9 | * version 2.1 of the License, or (at your option) any later version.
10 | *
11 | * This library is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 | * Lesser General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Lesser General Public
17 | * License along with this library; if not, write to the Free Software
18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 | *
20 | * $Id: $
21 | */
22 |
23 | package org.jmrtd.cert
24 |
25 | import java.security.InvalidAlgorithmParameterException
26 | import java.security.KeyStore
27 | import java.security.KeyStoreException
28 | import java.security.cert.CRL
29 | import java.security.cert.CRLSelector
30 | import java.security.cert.CertSelector
31 | import java.security.cert.CertStoreException
32 | import java.security.cert.CertStoreParameters
33 | import java.security.cert.CertStoreSpi
34 | import java.security.cert.Certificate
35 | import java.util.ArrayList
36 | import java.util.Enumeration
37 |
38 | /**
39 | * Certificate store backed by key store.
40 | *
41 | * @author The JMRTD team (info@jmrtd.org)
42 | *
43 | * @version $Revision: $
44 | */
45 | class KeyStoreCertStoreSpi @Throws(InvalidAlgorithmParameterException::class)
46 | constructor(params: CertStoreParameters) : CertStoreSpi(params) {
47 |
48 | private val keyStore: KeyStore
49 |
50 | init {
51 | keyStore = (params as KeyStoreCertStoreParameters).keyStore
52 | }
53 |
54 | @Throws(CertStoreException::class)
55 | override fun engineGetCertificates(selector: CertSelector): Collection {
56 | try {
57 | val certificates = ArrayList(keyStore.size())
58 | val aliases = keyStore.aliases()
59 | while (aliases.hasMoreElements()) {
60 | val alias = aliases.nextElement() as String
61 | if (keyStore.isCertificateEntry(alias)) {
62 | val certificate = keyStore.getCertificate(alias)
63 | if (selector.match(certificate)) {
64 | certificates.add(certificate)
65 | }
66 | }
67 | }
68 | return certificates
69 | } catch (kse: KeyStoreException) {
70 | throw CertStoreException(kse.message)
71 | }
72 |
73 | }
74 |
75 | @Throws(CertStoreException::class)
76 | override fun engineGetCRLs(selector: CRLSelector): Collection {
77 | return ArrayList(0)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/java/org/jmrtd/cert/PKDCertStoreParameters.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * JMRTD - A Java API for accessing machine readable travel documents.
3 | *
4 | * Copyright (C) 2006 - 2013 The JMRTD team
5 | *
6 | * This library is free software; you can redistribute it and/or
7 | * modify it under the terms of the GNU Lesser General Public
8 | * License as published by the Free Software Foundation; either
9 | * version 2.1 of the License, or (at your option) any later version.
10 | *
11 | * This library is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 | * Lesser General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Lesser General Public
17 | * License along with this library; if not, write to the Free Software
18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 | *
20 | * $Id: $
21 | */
22 |
23 | package org.jmrtd.cert
24 |
25 | import java.security.cert.CertStoreParameters
26 |
27 | /**
28 | * Parameters for PKD backed certificate store.
29 | *
30 | * @author The JMRTD team (info@jmrtd.org)
31 | *
32 | * @version $Revision: $
33 | */
34 | open class PKDCertStoreParameters @JvmOverloads constructor(
35 | /**
36 | * @return the serverName
37 | */
38 | val serverName: String = DEFAULT_SERVER_NAME,
39 | /**
40 | * @return the port
41 | */
42 | val port: Int = DEFAULT_PORT,
43 | /**
44 | * @return the baseDN
45 | */
46 | val baseDN: String = DEFAULT_BASE_DN) : Cloneable, CertStoreParameters {
47 |
48 | constructor(serverName: String, baseDN: String) : this(serverName, DEFAULT_PORT, baseDN)
49 |
50 | /**
51 | * Makes a copy of this object.
52 | *
53 | * @return a copy of this object
54 | */
55 | override fun clone(): Any {
56 | return PKDCertStoreParameters(serverName, port, baseDN)
57 | }
58 |
59 | override fun toString(): String {
60 | return "PKDCertStoreParameters [$serverName:$port/$baseDN]"
61 | }
62 |
63 | override fun equals(otherObj: Any?): Boolean {
64 | if (otherObj == null) {
65 | return false
66 | }
67 | if (otherObj === this) {
68 | return true
69 | }
70 | if (this.javaClass != otherObj.javaClass) {
71 | return false
72 | }
73 | val otherParams = otherObj as PKDCertStoreParameters?
74 | return (otherParams!!.serverName == this.serverName
75 | && otherParams.port == this.port
76 | && otherParams.baseDN == this.baseDN)
77 | }
78 |
79 | override fun hashCode(): Int {
80 | return (serverName.hashCode() + port + baseDN.hashCode()) * 2 + 303
81 | }
82 |
83 | companion object {
84 |
85 | private val DEFAULT_SERVER_NAME = "localhost"
86 | private val DEFAULT_PORT = 389
87 | private val DEFAULT_BASE_DN = "dc=data,dc=pkdDownload"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/network/MasterListService.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.network
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import io.reactivex.Single
6 | import io.reactivex.android.schedulers.AndroidSchedulers
7 | import io.reactivex.schedulers.Schedulers
8 | import okhttp3.OkHttpClient
9 | import okhttp3.logging.HttpLoggingInterceptor
10 | import org.jmrtd.cert.CSCAMasterList
11 | import retrofit2.Retrofit
12 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
13 | import retrofit2.converter.gson.GsonConverterFactory
14 | import java.io.ByteArrayInputStream
15 | import java.nio.charset.Charset
16 | import java.security.cert.Certificate
17 | import java.util.concurrent.TimeUnit
18 | import java.util.zip.ZipInputStream
19 |
20 | class MasterListService constructor(var context: Context, var baseUrl: String) {
21 |
22 | private lateinit var api: MasterListApi
23 | init {
24 | initRetrofit()
25 | }
26 |
27 | fun getSpanishMasterList(): Single> {
28 | return api.getSpanishMasterList()
29 | .flatMap { result ->
30 | val certificates = ArrayList()
31 | val byteStream = result.byteStream()
32 | val zipInputStream = ZipInputStream(byteStream)
33 | var entry = zipInputStream.nextEntry
34 | while (entry != null) {
35 | val name = entry.name
36 | if (!entry.isDirectory) {
37 | try {
38 | val readBytes = zipInputStream.readBytes()
39 | val cscaMasterList = CSCAMasterList(readBytes)
40 | certificates.addAll(cscaMasterList.getCertificates())
41 | } catch (e: Exception) {
42 | e.printStackTrace()
43 | // throw Exception("Unable to extract the zip file: " + name)
44 | } finally {
45 | }
46 | }
47 | entry = zipInputStream.nextEntry
48 | }
49 |
50 | Single.fromCallable{certificates}
51 | }
52 | }
53 |
54 | private fun initRetrofit() {
55 |
56 | val httpLoggingInterceptor = HttpLoggingInterceptor()
57 | val httpClient = OkHttpClient.Builder()
58 | .addInterceptor(httpLoggingInterceptor.apply { httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BASIC })
59 | .readTimeout(120, TimeUnit.SECONDS)
60 | .connectTimeout(120, TimeUnit.SECONDS)
61 | .build()
62 |
63 | api = Retrofit.Builder()
64 | .baseUrl(baseUrl)
65 | .client(httpClient)
66 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
67 | .addConverterFactory(GsonConverterFactory.create())
68 | .build()
69 | .create(MasterListApi::class.java)
70 | }
71 | }
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 |
5 | android {
6 | compileSdkVersion 33
7 | defaultConfig {
8 | applicationId "example.jllarraz.com.passportreader"
9 | minSdkVersion 23
10 | targetSdkVersion 33
11 | versionCode 1
12 | versionName "1.0"
13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14 | multiDexEnabled true
15 | }
16 | buildTypes {
17 | release {
18 | minifyEnabled false
19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
20 | }
21 | debug {
22 | debuggable true
23 | jniDebuggable true
24 | }
25 | }
26 |
27 | compileOptions {
28 | sourceCompatibility JavaVersion.VERSION_1_8
29 | targetCompatibility JavaVersion.VERSION_1_8
30 | }
31 |
32 | packagingOptions {
33 | exclude 'META-INF/proguard/androidx-annotations.pro'
34 | exclude 'META-INF/androidx.exifinterface_exifinterface.version'
35 | }
36 |
37 | buildFeatures {
38 | viewBinding true
39 | }
40 | }
41 |
42 | dependencies {
43 | implementation fileTree(include: ['*.jar'], dir: 'libs')
44 | implementation 'androidx.appcompat:appcompat:1.6.1'
45 | implementation 'com.google.android.material:material:1.8.0'
46 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
47 | testImplementation 'junit:junit:4.13.2'
48 | androidTestImplementation 'androidx.test:runner:1.5.2'
49 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
50 | // ML Kit dependencies
51 | implementation 'com.google.firebase:firebase-core:21.1.1'
52 | implementation 'com.google.firebase:firebase-ml-common:22.1.2'
53 | implementation 'com.google.android.gms:play-services-mlkit-text-recognition:18.0.2'
54 |
55 | //NFC Passport
56 | implementation 'org.jmrtd:jmrtd:0.7.35'
57 | implementation 'com.madgag.spongycastle:prov:1.58.0.0'
58 | implementation 'net.sf.scuba:scuba-sc-android:0.0.23'
59 | implementation ('org.ejbca.cvc:cert-cvc:1.4.13'){
60 | exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on'
61 | }
62 |
63 | //WSQ
64 | implementation 'com.github.mhshams:jnbis:2.0.2'
65 |
66 | //Input data Validator
67 | implementation 'com.mobsandgeeks:android-saripaar:2.0.3'
68 |
69 | //DatatypeConverter
70 | implementation 'commons-codec:commons-codec:1.13'
71 |
72 | //Camera
73 | implementation 'io.fotoapparat:fotoapparat:2.7.0'
74 |
75 | implementation 'androidx.multidex:multidex:2.0.1'
76 |
77 | //RX
78 | implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
79 | implementation 'io.reactivex.rxjava2:rxjava:2.2.19'
80 |
81 | //Annotations
82 | implementation "org.androidannotations:androidannotations-api:4.4.0"
83 |
84 | //OpenLDAP
85 | //implementation 'com.unboundid:unboundid-ldapsdk:5.0.1@jar'
86 |
87 | //OKHttp
88 | implementation 'com.squareup.okhttp3:okhttp:4.4.0'
89 | implementation "com.squareup.okhttp3:okhttp-urlconnection:4.4.0"
90 | implementation 'com.squareup.okhttp3:logging-interceptor:4.4.0'
91 |
92 | //Retrofit
93 | implementation 'com.squareup.retrofit2:retrofit:2.9.0'
94 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
95 | implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
96 | }
97 |
98 | apply plugin: 'com.google.gms.google-services'
99 |
--------------------------------------------------------------------------------
/app/src/main/java/org/jmrtd/cert/KeyStoreCertStoreParameters.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * JMRTD - A Java API for accessing machine readable travel documents.
3 | *
4 | * Copyright (C) 2006 - 2013 The JMRTD team
5 | *
6 | * This library is free software; you can redistribute it and/or
7 | * modify it under the terms of the GNU Lesser General Public
8 | * License as published by the Free Software Foundation; either
9 | * version 2.1 of the License, or (at your option) any later version.
10 | *
11 | * This library is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 | * Lesser General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Lesser General Public
17 | * License along with this library; if not, write to the Free Software
18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 | *
20 | * $Id: $
21 | */
22 |
23 | package org.jmrtd.cert
24 |
25 | import java.io.IOException
26 | import java.io.InputStream
27 | import java.net.URI
28 | import java.net.URLConnection
29 | import java.security.KeyStore
30 | import java.security.KeyStoreException
31 | import java.security.cert.CertStoreParameters
32 | import java.util.logging.Logger
33 |
34 | import org.jmrtd.JMRTDSecurityProvider
35 |
36 | /**
37 | * Parameters for key store backed certificate store.
38 | *
39 | * @author The JMRTD team (info@jmrtd.org)
40 | *
41 | * @version $Revision: $
42 | */
43 | class KeyStoreCertStoreParameters(val keyStore: KeyStore) : Cloneable, CertStoreParameters {
44 |
45 | @Throws(KeyStoreException::class)
46 | constructor(uri: URI, password: CharArray) : this(uri, DEFAULT_ALGORITHM, password)
47 |
48 | @Throws(KeyStoreException::class)
49 | @JvmOverloads
50 | constructor(uri: URI, algorithm: String = DEFAULT_ALGORITHM, password: CharArray = DEFAULT_PASSWORD) : this(readKeyStore(uri, algorithm, password))
51 |
52 | /**
53 | * Makes a shallow copy of this object as this
54 | * class is immutable.
55 | *
56 | * @return a shallow copy of this object
57 | */
58 | override fun clone(): Any {
59 | return KeyStoreCertStoreParameters(keyStore)
60 | }
61 |
62 | companion object {
63 |
64 | private val LOGGER = Logger.getLogger("org.jmrtd")
65 |
66 | private val DEFAULT_ALGORITHM = "JKS"
67 | private val DEFAULT_PASSWORD = "".toCharArray()
68 |
69 | @Throws(KeyStoreException::class)
70 | private fun readKeyStore(location: URI, keyStoreType: String, password: CharArray): KeyStore {
71 | try {
72 | val n = JMRTDSecurityProvider.beginPreferBouncyCastleProvider()
73 | val uc = location.toURL().openConnection()
74 | val inputStream = uc.getInputStream()
75 | var ks: KeyStore? = null
76 | ks = KeyStore.getInstance(keyStoreType)
77 | try {
78 | LOGGER.info("KeystoreCertStore will use provider for KeyStore: " + ks!!.provider.javaClass.canonicalName!!)
79 | ks.load(inputStream, password)
80 | } catch (ioe: IOException) {
81 | LOGGER.warning("Cannot read this file \"$location\" as keystore")
82 | // ioe.printStackTrace();
83 | }
84 |
85 | inputStream.close()
86 | JMRTDSecurityProvider.endPreferBouncyCastleProvider(n)
87 | return ks
88 | } catch (e: Exception) {
89 | // e.printStackTrace();
90 | throw KeyStoreException("Error getting keystore: " + e.message)
91 | }
92 |
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/app/src/main/java/org/jmrtd/FeatureStatus.kt:
--------------------------------------------------------------------------------
1 | package org.jmrtd
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 |
6 | /**
7 | * Security features of this identity document.
8 | *
9 | * @author The JMRTD team (info@jmrtd.org)
10 | *
11 | * @version $Revision: 1559 $
12 | */
13 | class FeatureStatus : Parcelable {
14 |
15 | private var hasSAC: Verdict? = null
16 | private var hasBAC: Verdict? = null
17 | private var hasAA: Verdict? = null
18 | private var hasEAC: Verdict? = null
19 | private var hasCA: Verdict? = null
20 |
21 | /**
22 | * Outcome of a feature presence check.
23 | *
24 | * @author The JMRTD team (info@jmrtd.org)
25 | *
26 | * @version $Revision: 1559 $
27 | */
28 | enum class Verdict {
29 | UNKNOWN, /* Presence unknown */
30 | PRESENT, /* Present */
31 | NOT_PRESENT
32 | /* Not present */
33 | }
34 |
35 | constructor() {
36 | this.hasSAC = Verdict.UNKNOWN
37 | this.hasBAC = Verdict.UNKNOWN
38 | this.hasAA = Verdict.UNKNOWN
39 | this.hasEAC = Verdict.UNKNOWN
40 | this.hasCA = Verdict.UNKNOWN
41 | }
42 |
43 | fun setSAC(hasSAC: Verdict) {
44 | this.hasSAC = hasSAC
45 | }
46 |
47 | fun hasSAC(): Verdict? {
48 | return hasSAC
49 | }
50 |
51 |
52 | fun setBAC(hasBAC: Verdict) {
53 | this.hasBAC = hasBAC
54 | }
55 |
56 | fun hasBAC(): Verdict? {
57 | return hasBAC
58 | }
59 |
60 | fun setAA(hasAA: Verdict) {
61 | this.hasAA = hasAA
62 | }
63 |
64 | fun hasAA(): Verdict? {
65 | return hasAA
66 | }
67 |
68 | fun setEAC(hasEAC: Verdict) {
69 | this.hasEAC = hasEAC
70 | }
71 |
72 | fun hasEAC(): Verdict? {
73 | return hasEAC
74 | }
75 |
76 | fun setCA(hasCA: Verdict) {
77 | this.hasCA = hasCA
78 | }
79 |
80 | fun hasCA(): Verdict? {
81 | return hasCA
82 | }
83 |
84 | constructor(`in`: Parcel) {
85 | this.hasSAC = if(`in`.readInt() == 1){ Verdict.valueOf(`in`.readString()!!) } else { null }
86 | this.hasBAC = if(`in`.readInt() == 1){Verdict.valueOf(`in`.readString()!!) } else { null }
87 | this.hasAA = if(`in`.readInt() == 1){Verdict.valueOf(`in`.readString()!!) } else { null }
88 | this.hasEAC = if(`in`.readInt() == 1){Verdict.valueOf(`in`.readString()!!) } else { null }
89 | this.hasCA = if(`in`.readInt() == 1){Verdict.valueOf(`in`.readString()!!) } else { null }
90 | }
91 |
92 | override fun describeContents(): Int {
93 | return 0
94 | }
95 |
96 | override fun writeToParcel(dest: Parcel, flags: Int) {
97 | dest.writeInt(if(this.hasSAC!=null) 1 else 0)
98 | if(this.hasSAC!=null) {
99 | dest.writeString(this.hasSAC?.name)
100 | }
101 | dest.writeInt(if(this.hasBAC!=null) 1 else 0)
102 | if(this.hasBAC!=null) {
103 | dest.writeString(this.hasBAC?.name)
104 | }
105 | dest.writeInt(if(this.hasAA!=null) 1 else 0)
106 | if(this.hasAA!=null) {
107 | dest.writeString(this.hasAA?.name)
108 | }
109 | dest.writeInt(if(this.hasEAC!=null) 1 else 0)
110 | if(this.hasEAC!=null) {
111 | dest.writeString(this.hasEAC?.name)
112 | }
113 | dest.writeInt(if(this.hasCA!=null) 1 else 0)
114 | if(this.hasCA!=null) {
115 | dest.writeString(this.hasCA?.name)
116 | }
117 | }
118 |
119 | companion object {
120 | @JvmField
121 | val CREATOR: Parcelable.Creator<*> = object : Parcelable.Creator {
122 | override fun createFromParcel(pc: Parcel): FeatureStatus {
123 | return FeatureStatus(pc)
124 | }
125 |
126 | override fun newArray(size: Int): Array {
127 | return arrayOfNulls(size)
128 | }
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | xmlns:android
17 |
18 | ^$
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | xmlns:.*
28 |
29 | ^$
30 |
31 |
32 | BY_NAME
33 |
34 |
35 |
36 |
37 |
38 |
39 | .*:id
40 |
41 | http://schemas.android.com/apk/res/android
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | .*:name
51 |
52 | http://schemas.android.com/apk/res/android
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | name
62 |
63 | ^$
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | style
73 |
74 | ^$
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | .*
84 |
85 | ^$
86 |
87 |
88 | BY_NAME
89 |
90 |
91 |
92 |
93 |
94 |
95 | .*
96 |
97 | http://schemas.android.com/apk/res/android
98 |
99 |
100 | ANDROID_ATTRIBUTE_ORDER
101 |
102 |
103 |
104 |
105 |
106 |
107 | .*
108 |
109 | .*
110 |
111 |
112 | BY_NAME
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
22 |
23 |
24 |
27 |
28 |
33 |
37 |
38 |
44 |
47 |
48 |
51 |
52 |
59 |
60 |
64 |
77 |
80 |
83 |
88 |
89 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/ui/activities/SelectionActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package example.jllarraz.com.passportreader.ui.activities
18 |
19 |
20 | import android.app.Activity
21 | import android.content.Intent
22 | import android.os.Bundle
23 | import androidx.appcompat.app.AppCompatActivity
24 | import example.jllarraz.com.passportreader.R
25 | import example.jllarraz.com.passportreader.common.IntentData
26 | import example.jllarraz.com.passportreader.ui.fragments.SelectionFragment
27 | import org.jmrtd.lds.icao.MRZInfo
28 |
29 | class SelectionActivity : AppCompatActivity(), SelectionFragment.SelectionFragmentListener {
30 |
31 | override fun onCreate(savedInstanceState: Bundle?) {
32 | super.onCreate(savedInstanceState)
33 | setContentView(R.layout.activity_camera)
34 | if (null == savedInstanceState) {
35 | supportFragmentManager.beginTransaction()
36 | .replace(R.id.container, SelectionFragment(), TAG_SELECTION_FRAGMENT)
37 | .commit()
38 | }
39 | }
40 |
41 |
42 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
43 | var data = data
44 | if (data == null) {
45 | data = Intent()
46 | }
47 | when (requestCode) {
48 | REQUEST_MRZ -> {
49 | when (resultCode) {
50 | Activity.RESULT_OK -> {
51 | onPassportRead(data.getSerializableExtra(IntentData.KEY_MRZ_INFO) as MRZInfo)
52 | }
53 | Activity.RESULT_CANCELED -> {
54 | val fragmentByTag = supportFragmentManager.findFragmentByTag(TAG_SELECTION_FRAGMENT)
55 | if (fragmentByTag is SelectionFragment) {
56 | fragmentByTag.selectManualToggle()
57 | }
58 | }
59 | else -> {
60 | val fragmentByTag = supportFragmentManager.findFragmentByTag(TAG_SELECTION_FRAGMENT)
61 | if (fragmentByTag is SelectionFragment) {
62 | fragmentByTag.selectManualToggle()
63 | }
64 | }
65 | }
66 | }
67 | REQUEST_NFC -> {
68 | val fragmentByTag = supportFragmentManager.findFragmentByTag(TAG_SELECTION_FRAGMENT)
69 | if (fragmentByTag is SelectionFragment) {
70 | fragmentByTag.selectManualToggle()
71 | }
72 | }
73 | }
74 | super.onActivityResult(requestCode, resultCode, data)
75 | }
76 |
77 | private fun test() {
78 | //Method to test NFC without rely into the Camera
79 | val TEST_LINE_1 = "P = object : Parcelable.Creator {
112 | override fun createFromParcel(pc: Parcel): PersonDetails {
113 | return PersonDetails(pc)
114 | }
115 |
116 | override fun newArray(size: Int): Array {
117 | return arrayOfNulls(size)
118 | }
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/utils/MRZUtil.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.utils
2 |
3 |
4 | import org.jmrtd.lds.icao.MRZInfo
5 |
6 | import java.util.ArrayList
7 |
8 | object MRZUtil {
9 |
10 | val TAG = MRZUtil::class.java.simpleName
11 |
12 | private val PASSPORT_LINE_1 = "[P]{1}[A-Z<]{1}[A-Z<]{3}[A-Z0-9<]{39}$"
13 | private val PASSPORT_LINE_2 = "[A-Z0-9<]{9}[0-9]{1}[A-Z<]{3}[0-9]{6}[0-9]{1}[FM<]{1}[0-9]{6}[0-9]{1}[A-Z0-9<]{14}[0-9<]{1}[0-9]{1}$"
14 |
15 | var mLines1 = ArrayList()
16 | var mLines2 = ArrayList()
17 |
18 | val mrzInfo: MRZInfo
19 | @Throws(IllegalArgumentException::class)
20 | get() {
21 | val iteratorLine1 = mLines1.iterator()
22 | while (iteratorLine1.hasNext()) {
23 | val line1 = iteratorLine1.next()
24 | val iteratorLine2 = mLines2.iterator()
25 | while (iteratorLine2.hasNext()) {
26 | val line2 = iteratorLine2.next()
27 | try {
28 | return MRZInfo(line1 + "\n" + line2)
29 | } catch (e: Exception) {
30 | }
31 |
32 | }
33 | }
34 | throw IllegalArgumentException("Unable to find a combination of lines that pass MRZ checksum")
35 | }
36 |
37 |
38 | @Throws(IllegalArgumentException::class)
39 | fun cleanString(mrz: String): String {
40 | val lines = mrz.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
41 | if (lines.size > 2) {
42 | return cleanLine1(lines[0]) + "\n" + cleanLine2(lines[1])
43 | }
44 | throw IllegalArgumentException("Not enough lines")
45 | }
46 |
47 | @Throws(IllegalArgumentException::class)
48 | fun cleanLine1(line: String?): String {
49 | if (line == null || line.length != 44) {
50 | throw IllegalArgumentException("Line 1 doesnt have the right length")
51 | }
52 | val group1 = line.substring(0, 2)
53 | var group2 = line.substring(2, 5)
54 | val group3 = line.substring(5, line.length)
55 |
56 | group2 = replaceNumberWithAlfa(group2)
57 |
58 |
59 | return group1 + group2 + group3
60 | }
61 |
62 | @Throws(IllegalArgumentException::class)
63 | fun cleanLine2(line: String?): String {
64 | if (line == null || line.length != 44) {
65 | throw IllegalArgumentException("Line 2 doesnt have the right length")
66 | }
67 |
68 | val group1 = line.substring(0, 9)
69 | var group2 = line.substring(9, 10)
70 | var group3 = line.substring(10, 13)
71 | var group4 = line.substring(13, 19)
72 | var group5 = line.substring(19, 20)
73 | val group6 = line.substring(20, 21)
74 | var group7 = line.substring(21, 27)
75 | var group8 = line.substring(27, 28)
76 | val group9 = line.substring(28, 42)
77 | var group10 = line.substring(42, 43)
78 | var group11 = line.substring(43, 44)
79 |
80 | group2 = replaceAlfaWithNumber(group2)
81 | group3 = replaceNumberWithAlfa(group3)
82 | group4 = replaceAlfaWithNumber(group4)
83 | group5 = replaceAlfaWithNumber(group5)
84 | group7 = replaceAlfaWithNumber(group7)
85 | group8 = replaceAlfaWithNumber(group8)
86 | group10 = replaceAlfaWithNumber(group10)
87 | group11 = replaceAlfaWithNumber(group11)
88 |
89 | return group1 + group2 + group3 + group4 + group5 + group6 + group7 + group8 + group9 + group10 + group11
90 | }
91 |
92 | fun replaceNumberWithAlfa(str: String): String {
93 | var str = str
94 | str = str.replace("0".toRegex(), "O")
95 | str = str.replace("1".toRegex(), "I")
96 | str = str.replace("2".toRegex(), "Z")
97 | str = str.replace("5".toRegex(), "S")
98 | return str
99 | }
100 |
101 | fun replaceAlfaWithNumber(str: String): String {
102 | var str = str
103 | str = str.replace("O".toRegex(), "0")
104 | str = str.replace("I".toRegex(), "1")
105 | str = str.replace("Z".toRegex(), "2")
106 | str = str.replace("S".toRegex(), "5")
107 | return str
108 | }
109 |
110 | fun addLine1(line1: String) {
111 | if (!mLines1.contains(line1)) {
112 | mLines1.add(line1)
113 | }
114 | }
115 |
116 | fun addLine2(line2: String) {
117 | if (!mLines2.contains(line2)) {
118 | mLines2.add(line2)
119 | }
120 | }
121 |
122 | fun cleanStorage() {
123 | mLines1.clear()
124 | mLines2.clear()
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/data/AdditionalDocumentDetails.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.data
2 |
3 | import android.graphics.Bitmap
4 | import android.os.Parcel
5 | import android.os.Parcelable
6 |
7 | import java.util.ArrayList
8 | import java.util.Date
9 |
10 | class AdditionalDocumentDetails : Parcelable {
11 |
12 | var endorsementsAndObservations: String? = null
13 | var dateAndTimeOfPersonalization: String? = null
14 | var dateOfIssue: String? = null
15 | var imageOfFront: Bitmap? = null
16 | var imageOfRear: Bitmap? = null
17 | var issuingAuthority: String? = null
18 | var namesOfOtherPersons: List? = null
19 | var personalizationSystemSerialNumber: String? = null
20 | var taxOrExitRequirements: String? = null
21 | var tag: Int = 0
22 | var tagPresenceList: List? = null
23 |
24 |
25 | constructor() {
26 | namesOfOtherPersons = ArrayList()
27 | tagPresenceList = ArrayList()
28 | }
29 |
30 | constructor(`in`: Parcel) {
31 |
32 | namesOfOtherPersons = ArrayList()
33 | tagPresenceList = ArrayList()
34 |
35 | this.endorsementsAndObservations = if (`in`.readInt() == 1) `in`.readString() else null
36 | this.dateAndTimeOfPersonalization = if (`in`.readInt() == 1) `in`.readString() else null
37 | this.dateOfIssue = if (`in`.readInt() == 1) `in`.readString() else null
38 |
39 | this.imageOfFront = if (`in`.readInt() == 1) `in`.readParcelable(Bitmap::class.java.classLoader) else null
40 | this.imageOfRear = if (`in`.readInt() == 1) `in`.readParcelable(Bitmap::class.java.classLoader) else null
41 | this.issuingAuthority = if (`in`.readInt() == 1) `in`.readString() else null
42 |
43 | if (`in`.readInt() == 1) {
44 | `in`.readList(namesOfOtherPersons!!, String::class.java.classLoader)
45 | }
46 |
47 | this.personalizationSystemSerialNumber = if (`in`.readInt() == 1) `in`.readString() else null
48 | this.taxOrExitRequirements = if (`in`.readInt() == 1) `in`.readString() else null
49 |
50 | tag = `in`.readInt()
51 | if (`in`.readInt() == 1) {
52 | `in`.readList(tagPresenceList!!, Int::class.java.classLoader)
53 | }
54 |
55 |
56 | }
57 |
58 | override fun describeContents(): Int {
59 | return 0
60 | }
61 |
62 | override fun writeToParcel(dest: Parcel, flags: Int) {
63 | dest.writeInt(if (endorsementsAndObservations != null) 1 else 0)
64 | if (endorsementsAndObservations != null) {
65 | dest.writeString(endorsementsAndObservations)
66 | }
67 |
68 | dest.writeInt(if (dateAndTimeOfPersonalization != null) 1 else 0)
69 | if (dateAndTimeOfPersonalization != null) {
70 | dest.writeString(dateAndTimeOfPersonalization)
71 | }
72 |
73 | dest.writeInt(if (dateOfIssue != null) 1 else 0)
74 | if (dateOfIssue != null) {
75 | dest.writeString(dateOfIssue)
76 | }
77 |
78 | dest.writeInt(if (imageOfFront != null) 1 else 0)
79 | if (imageOfFront != null) {
80 | dest.writeParcelable(imageOfFront, flags)
81 | }
82 |
83 | dest.writeInt(if (imageOfRear != null) 1 else 0)
84 | if (imageOfRear != null) {
85 | dest.writeParcelable(imageOfRear, flags)
86 | }
87 |
88 | dest.writeInt(if (issuingAuthority != null) 1 else 0)
89 | if (issuingAuthority != null) {
90 | dest.writeString(issuingAuthority)
91 | }
92 |
93 | dest.writeInt(if (namesOfOtherPersons != null) 1 else 0)
94 | if (namesOfOtherPersons != null) {
95 | dest.writeList(namesOfOtherPersons)
96 | }
97 |
98 | dest.writeInt(if (personalizationSystemSerialNumber != null) 1 else 0)
99 | if (personalizationSystemSerialNumber != null) {
100 | dest.writeString(personalizationSystemSerialNumber)
101 | }
102 |
103 | dest.writeInt(if (taxOrExitRequirements != null) 1 else 0)
104 | if (taxOrExitRequirements != null) {
105 | dest.writeString(taxOrExitRequirements)
106 | }
107 |
108 | dest.writeInt(tag)
109 | dest.writeInt(if (tagPresenceList != null) 1 else 0)
110 | if (tagPresenceList != null) {
111 | dest.writeList(tagPresenceList)
112 | }
113 |
114 |
115 | }
116 |
117 | companion object {
118 | @JvmField
119 | val CREATOR: Parcelable.Creator<*> = object : Parcelable.Creator {
120 | override fun createFromParcel(pc: Parcel): AdditionalDocumentDetails {
121 | return AdditionalDocumentDetails(pc)
122 | }
123 |
124 | override fun newArray(size: Int): Array {
125 | return arrayOfNulls(size)
126 | }
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/data/Passport.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.data
2 |
3 | import android.graphics.Bitmap
4 | import android.os.Parcel
5 | import android.os.Parcelable
6 |
7 | import org.jmrtd.FeatureStatus
8 | import org.jmrtd.VerificationStatus
9 | import org.jmrtd.lds.SODFile
10 |
11 | import java.util.ArrayList
12 | import java.util.HashMap
13 |
14 | class Passport : Parcelable {
15 |
16 | var sodFile: SODFile? = null
17 | var face: Bitmap? = null
18 | var portrait: Bitmap? = null
19 | var signature: Bitmap? = null
20 | var fingerprints: List? = null
21 | var personDetails: PersonDetails? = null
22 | var additionalPersonDetails: AdditionalPersonDetails? = null
23 | var additionalDocumentDetails: AdditionalDocumentDetails? = null
24 | var featureStatus: FeatureStatus? = null
25 | var verificationStatus: VerificationStatus? = null
26 |
27 | constructor(`in`: Parcel) {
28 |
29 |
30 | fingerprints = ArrayList()
31 | this.face = if (`in`.readInt() == 1) `in`.readParcelable(Bitmap::class.java.classLoader) else null
32 | this.portrait = if (`in`.readInt() == 1) `in`.readParcelable(Bitmap::class.java.classLoader) else null
33 | this.personDetails = if (`in`.readInt() == 1) `in`.readParcelable(PersonDetails::class.java.classLoader) else null
34 | this.additionalPersonDetails = if (`in`.readInt() == 1) `in`.readParcelable(AdditionalPersonDetails::class.java.classLoader) else null
35 |
36 | if (`in`.readInt() == 1) {
37 | `in`.readList(fingerprints!!, Bitmap::class.java.classLoader)
38 | }
39 |
40 | this.signature = if (`in`.readInt() == 1) `in`.readParcelable(Bitmap::class.java.classLoader) else null
41 | this.additionalDocumentDetails = if (`in`.readInt() == 1) `in`.readParcelable(AdditionalDocumentDetails::class.java.classLoader) else null
42 | if (`in`.readInt() == 1) {
43 | sodFile = `in`.readSerializable() as SODFile
44 | }
45 |
46 | if (`in`.readInt() == 1) {
47 | featureStatus = `in`.readParcelable(FeatureStatus::class.java.classLoader)
48 | }
49 |
50 | if (`in`.readInt() == 1) {
51 | featureStatus = `in`.readParcelable(FeatureStatus::class.java.classLoader)
52 | }
53 |
54 | if (`in`.readInt() == 1) {
55 | verificationStatus = `in`.readParcelable(VerificationStatus::class.java.classLoader)
56 | }
57 |
58 | }
59 |
60 | constructor() {
61 | fingerprints = ArrayList()
62 | featureStatus = FeatureStatus()
63 | verificationStatus = VerificationStatus()
64 | }
65 |
66 | override fun describeContents(): Int {
67 | return 0
68 | }
69 |
70 | override fun writeToParcel(dest: Parcel, flags: Int) {
71 | dest.writeInt(if (face != null) 1 else 0)
72 | if (face != null) {
73 | dest.writeParcelable(face, flags)
74 | }
75 |
76 | dest.writeInt(if (portrait != null) 1 else 0)
77 | if (portrait != null) {
78 | dest.writeParcelable(portrait, flags)
79 | }
80 |
81 | dest.writeInt(if (personDetails != null) 1 else 0)
82 | if (personDetails != null) {
83 | dest.writeParcelable(personDetails, flags)
84 | }
85 |
86 | dest.writeInt(if (additionalPersonDetails != null) 1 else 0)
87 | if (additionalPersonDetails != null) {
88 | dest.writeParcelable(additionalPersonDetails, flags)
89 | }
90 |
91 | dest.writeInt(if (fingerprints != null) 1 else 0)
92 | if (fingerprints != null) {
93 | dest.writeList(fingerprints)
94 | }
95 |
96 | dest.writeInt(if (signature != null) 1 else 0)
97 | if (signature != null) {
98 | dest.writeParcelable(signature, flags)
99 | }
100 |
101 | dest.writeInt(if (additionalDocumentDetails != null) 1 else 0)
102 | if (additionalDocumentDetails != null) {
103 | dest.writeParcelable(additionalDocumentDetails, flags)
104 | }
105 |
106 | dest.writeInt(if (sodFile != null) 1 else 0)
107 | if (sodFile != null) {
108 | dest.writeSerializable(sodFile)
109 | }
110 |
111 | dest.writeInt(if (featureStatus != null) 1 else 0)
112 | if (featureStatus != null) {
113 | dest.writeParcelable(featureStatus, flags)
114 | }
115 |
116 | dest.writeInt(if (verificationStatus != null) 1 else 0)
117 | if (verificationStatus != null) {
118 | dest.writeParcelable(verificationStatus, flags)
119 | }
120 |
121 | }
122 |
123 | companion object {
124 |
125 | @JvmField
126 | val CREATOR: Parcelable.Creator<*> = object : Parcelable.Creator {
127 | override fun createFromParcel(pc: Parcel): Passport {
128 | return Passport(pc)
129 | }
130 |
131 | override fun newArray(size: Int): Array {
132 | return arrayOfNulls(size)
133 | }
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_nfc.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
15 |
16 |
20 |
21 |
25 |
26 |
34 |
35 |
41 |
42 |
45 |
46 |
53 |
54 |
55 |
58 |
59 |
66 |
67 |
68 |
71 |
72 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
97 |
98 |
107 |
108 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/ui/activities/NfcActivity.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.ui.activities
2 |
3 | import android.app.PendingIntent
4 | import android.content.Intent
5 | import android.graphics.Bitmap
6 | import android.nfc.NfcAdapter
7 | import android.os.Build
8 | import android.os.Bundle
9 | import android.provider.Settings
10 | import androidx.fragment.app.Fragment
11 | import androidx.fragment.app.FragmentActivity
12 | import android.widget.Toast
13 |
14 | import net.sf.scuba.smartcards.CardServiceException
15 |
16 |
17 | import org.jmrtd.lds.icao.MRZInfo
18 |
19 | import example.jllarraz.com.passportreader.R
20 | import example.jllarraz.com.passportreader.common.IntentData
21 | import example.jllarraz.com.passportreader.data.Passport
22 | import example.jllarraz.com.passportreader.ui.fragments.NfcFragment
23 | import example.jllarraz.com.passportreader.ui.fragments.PassportDetailsFragment
24 | import example.jllarraz.com.passportreader.ui.fragments.PassportPhotoFragment
25 |
26 | import example.jllarraz.com.passportreader.common.IntentData.KEY_MRZ_INFO
27 |
28 | class NfcActivity : androidx.fragment.app.FragmentActivity(), NfcFragment.NfcFragmentListener, PassportDetailsFragment.PassportDetailsFragmentListener, PassportPhotoFragment.PassportPhotoFragmentListener {
29 |
30 | private var mrzInfo: MRZInfo? = null
31 |
32 | private var nfcAdapter: NfcAdapter? = null
33 | private var pendingIntent: PendingIntent? = null
34 |
35 | override fun onCreate(savedInstanceState: Bundle?) {
36 | super.onCreate(savedInstanceState)
37 | setContentView(R.layout.activity_nfc)
38 | val intent = intent
39 | if (intent.hasExtra(IntentData.KEY_MRZ_INFO)) {
40 | mrzInfo = intent.getSerializableExtra(IntentData.KEY_MRZ_INFO) as MRZInfo
41 | } else {
42 | onBackPressed()
43 | }
44 |
45 | nfcAdapter = NfcAdapter.getDefaultAdapter(this)
46 |
47 | if (nfcAdapter == null) {
48 | Toast.makeText(this, getString(R.string.warning_no_nfc), Toast.LENGTH_SHORT).show()
49 | finish()
50 | return
51 | }
52 |
53 | pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
54 | PendingIntent.getActivity(this, 0, Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_MUTABLE)
55 | } else{
56 | PendingIntent.getActivity(this, 0, Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0)
57 | }
58 |
59 |
60 | if (null == savedInstanceState) {
61 | supportFragmentManager.beginTransaction()
62 | .replace(R.id.container, NfcFragment.newInstance(mrzInfo!!), TAG_NFC)
63 | .commit()
64 | }
65 | }
66 |
67 | public override fun onResume() {
68 | super.onResume()
69 |
70 | }
71 |
72 | public override fun onPause() {
73 | super.onPause()
74 |
75 | }
76 |
77 | public override fun onNewIntent(intent: Intent) {
78 | if (NfcAdapter.ACTION_TAG_DISCOVERED == intent.action || NfcAdapter.ACTION_TECH_DISCOVERED == intent.action) {
79 | // drop NFC events
80 | handleIntent(intent)
81 | }else{
82 | super.onNewIntent(intent)
83 | }
84 | }
85 |
86 | protected fun handleIntent(intent: Intent) {
87 | val fragmentByTag = supportFragmentManager.findFragmentByTag(TAG_NFC)
88 | if (fragmentByTag is NfcFragment) {
89 | fragmentByTag.handleNfcTag(intent)
90 | }
91 | }
92 |
93 |
94 | /////////////////////////////////////////////////////
95 | //
96 | // NFC Fragment events
97 | //
98 | /////////////////////////////////////////////////////
99 |
100 | override fun onEnableNfc() {
101 |
102 |
103 | if (nfcAdapter != null) {
104 | if (!nfcAdapter!!.isEnabled)
105 | showWirelessSettings()
106 |
107 | nfcAdapter!!.enableForegroundDispatch(this, pendingIntent, null, null)
108 | }
109 | }
110 |
111 | override fun onDisableNfc() {
112 | val nfcAdapter = NfcAdapter.getDefaultAdapter(this)
113 | nfcAdapter.disableForegroundDispatch(this)
114 | }
115 |
116 | override fun onPassportRead(passport: Passport?) {
117 | showFragmentDetails(passport!!)
118 | }
119 |
120 | override fun onCardException(cardException: Exception?) {
121 | //Toast.makeText(this, cardException.toString(), Toast.LENGTH_SHORT).show();
122 | //onBackPressed();
123 | }
124 |
125 | private fun showWirelessSettings() {
126 | Toast.makeText(this, getString(R.string.warning_enable_nfc), Toast.LENGTH_SHORT).show()
127 | val intent = Intent(Settings.ACTION_WIRELESS_SETTINGS)
128 | startActivity(intent)
129 | }
130 |
131 |
132 | private fun showFragmentDetails(passport: Passport) {
133 | supportFragmentManager.beginTransaction()
134 | .replace(R.id.container, PassportDetailsFragment.newInstance(passport))
135 | .addToBackStack(TAG_PASSPORT_DETAILS)
136 | .commit()
137 | }
138 |
139 | private fun showFragmentPhoto(bitmap: Bitmap) {
140 | supportFragmentManager.beginTransaction()
141 | .replace(R.id.container, PassportPhotoFragment.newInstance(bitmap))
142 | .addToBackStack(TAG_PASSPORT_PICTURE)
143 | .commit()
144 | }
145 |
146 |
147 | override fun onImageSelected(bitmap: Bitmap?) {
148 | showFragmentPhoto(bitmap!!)
149 | }
150 |
151 | companion object {
152 |
153 | private val TAG = NfcActivity::class.java.simpleName
154 |
155 |
156 | private val TAG_NFC = "TAG_NFC"
157 | private val TAG_PASSPORT_DETAILS = "TAG_PASSPORT_DETAILS"
158 | private val TAG_PASSPORT_PICTURE = "TAG_PASSPORT_PICTURE"
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_selection.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
20 |
21 |
28 |
29 |
34 |
35 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
60 |
61 |
66 |
67 |
76 |
77 |
78 |
79 |
84 |
85 |
94 |
95 |
96 |
97 |
102 |
103 |
112 |
113 |
114 |
115 |
120 |
121 |
126 |
127 |
132 |
133 |
134 |
135 |
143 |
144 |
145 |
146 |
147 |
148 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/utils/ImageUtil.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.utils
2 |
3 | import android.content.Context
4 | import android.graphics.*
5 | import android.media.Image
6 | import android.util.Log
7 | import androidx.annotation.Nullable
8 | import example.jllarraz.com.passportreader.mlkit.FrameMetadata
9 |
10 | import org.jnbis.internal.WsqDecoder
11 |
12 | import java.io.BufferedInputStream
13 | import java.io.ByteArrayInputStream
14 | import java.io.ByteArrayOutputStream
15 | import java.io.DataInputStream
16 | import java.io.File
17 | import java.io.FileInputStream
18 | import java.io.FileOutputStream
19 | import java.io.IOException
20 | import java.io.InputStream
21 | import java.io.OutputStream
22 | import java.nio.ByteBuffer
23 |
24 | import jj2000.j2k.decoder.Decoder
25 | import jj2000.j2k.util.ParameterList
26 |
27 |
28 | import org.jmrtd.lds.ImageInfo.WSQ_MIME_TYPE
29 | import kotlin.experimental.and
30 |
31 | object ImageUtil {
32 |
33 | private val TAG = ImageUtil::class.java.simpleName
34 |
35 | var JPEG_MIME_TYPE = "image/jpeg"
36 | var JPEG2000_MIME_TYPE = "image/jp2"
37 | var JPEG2000_ALT_MIME_TYPE = "image/jpeg2000"
38 | var WSQ_MIME_TYPE = "image/x-wsq"
39 |
40 | fun imageToByteArray(image: Image): ByteArray? {
41 | var data: ByteArray? = null
42 | if (image.format == ImageFormat.JPEG) {
43 | val planes = image.planes
44 | val buffer = planes[0].buffer
45 | data = ByteArray(buffer.capacity())
46 | buffer.get(data)
47 | return data
48 | } else if (image.format == ImageFormat.YUV_420_888) {
49 | data = NV21toJPEG(
50 | YUV_420_888toNV21(image),
51 | image.width, image.height)
52 | }
53 | return data
54 | }
55 |
56 | fun YUV_420_888toNV21(image: Image): ByteArray {
57 | val nv21: ByteArray
58 | val yBuffer = image.planes[0].buffer
59 | val uBuffer = image.planes[1].buffer
60 | val vBuffer = image.planes[2].buffer
61 |
62 | val ySize = yBuffer.remaining()
63 | val uSize = uBuffer.remaining()
64 | val vSize = vBuffer.remaining()
65 |
66 | nv21 = ByteArray(ySize + uSize + vSize)
67 |
68 | //U and V are swapped
69 | yBuffer.get(nv21, 0, ySize)
70 | vBuffer.get(nv21, ySize, vSize)
71 | uBuffer.get(nv21, ySize + vSize, uSize)
72 |
73 | return nv21
74 | }
75 |
76 | private fun NV21toJPEG(nv21: ByteArray, width: Int, height: Int): ByteArray {
77 | val out = ByteArrayOutputStream()
78 | val yuv = YuvImage(nv21, ImageFormat.NV21, width, height, null)
79 | yuv.compressToJpeg(Rect(0, 0, width, height), 100, out)
80 | return out.toByteArray()
81 | }
82 |
83 |
84 | /* IMAGE DECODIFICATION METHODS */
85 |
86 |
87 | @Throws(IOException::class)
88 | fun decodeImage(inputStream: InputStream, imageLength: Int, mimeType: String): Bitmap {
89 | var inputStream = inputStream
90 | /* DEBUG */
91 | synchronized(inputStream) {
92 | val dataIn = DataInputStream(inputStream)
93 | val bytes = ByteArray(imageLength)
94 | dataIn.readFully(bytes)
95 | inputStream = ByteArrayInputStream(bytes)
96 | }
97 | /* END DEBUG */
98 |
99 | if (JPEG2000_MIME_TYPE.equals(mimeType, ignoreCase = true) || JPEG2000_ALT_MIME_TYPE.equals(mimeType, ignoreCase = true)) {
100 | val bitmap = org.jmrtd.jj2000.JJ2000Decoder.decode(inputStream)
101 | return toAndroidBitmap(bitmap)
102 | } else if (WSQ_MIME_TYPE.equals(mimeType, ignoreCase = true)) {
103 | //org.jnbis.Bitmap bitmap = WSQDecoder.decode(inputStream);
104 | val wsqDecoder = WsqDecoder()
105 | val bitmap = wsqDecoder.decode(inputStream.readBytes())
106 | val byteData = bitmap.pixels
107 | val intData = IntArray(byteData.size)
108 | for (j in byteData.indices) {
109 | intData[j] = -0x1000000 or ((byteData[j].toInt() and 0xFF) shl 16) or ((byteData[j].toInt() and 0xFF) shl 8) or (byteData[j].toInt() and 0xFF)
110 | }
111 | return Bitmap.createBitmap(intData, 0, bitmap.width, bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
112 | //return toAndroidBitmap(bitmap);
113 | } else {
114 | return BitmapFactory.decodeStream(inputStream)
115 | }
116 | }
117 |
118 | fun rotateBitmap(source: Bitmap, angle: Float): Bitmap {
119 | val matrix = Matrix()
120 | matrix.postRotate(angle)
121 | return Bitmap.createBitmap(source, 0, 0, source.width, source.height, matrix, true)
122 | }
123 |
124 | // Convert NV21 format byte buffer to bitmap.
125 | @Nullable
126 | fun getBitmap(data: ByteBuffer, metadata: FrameMetadata): Bitmap? {
127 | data.rewind()
128 | val imageInBuffer = ByteArray(data.limit())
129 | data.get(imageInBuffer, 0, imageInBuffer.size)
130 | try {
131 | val image = YuvImage(
132 | imageInBuffer, ImageFormat.NV21, metadata.width, metadata.height, null
133 | )
134 | if (image != null) {
135 | val stream = ByteArrayOutputStream()
136 | image.compressToJpeg(Rect(0, 0, metadata.width, metadata.height), 80, stream)
137 |
138 | val bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size())
139 |
140 | stream.close()
141 | return rotateBitmap(bmp, metadata.rotation.toFloat())
142 | }
143 | } catch (e: Exception) {
144 | Log.e("VisionProcessorBase", "Error: " + e.message)
145 | }
146 |
147 | return null
148 | }
149 |
150 | /* ONLY PRIVATE METHODS BELOW */
151 |
152 | private fun toAndroidBitmap(bitmap: org.jmrtd.jj2000.Bitmap): Bitmap {
153 | val intData = bitmap.pixels
154 | return Bitmap.createBitmap(intData, 0, bitmap.width, bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
155 |
156 | }
157 | }
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/data/AdditionalPersonDetails.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.data
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 |
6 | import java.util.ArrayList
7 | import java.util.Date
8 |
9 | class AdditionalPersonDetails : Parcelable {
10 |
11 | var custodyInformation: String? = null
12 | var fullDateOfBirth: String? = null
13 | var nameOfHolder: String? = null
14 | var otherNames: List? = null
15 | var otherValidTDNumbers: List? = null
16 | var permanentAddress: List? = null
17 | var personalNumber: String? = null
18 | var personalSummary: String? = null
19 | var placeOfBirth: List? = null
20 | var profession: String? = null
21 | var proofOfCitizenship: ByteArray? = null
22 | var tag: Int = 0
23 | var tagPresenceList: List? = null
24 | var telephone: String? = null
25 | var title: String? = null
26 |
27 | constructor() {
28 | otherNames = ArrayList()
29 | otherValidTDNumbers = ArrayList()
30 | permanentAddress = ArrayList()
31 | placeOfBirth = ArrayList()
32 | tagPresenceList = ArrayList()
33 | }
34 |
35 | constructor(`in`: Parcel) {
36 |
37 | otherNames = ArrayList()
38 | otherValidTDNumbers = ArrayList()
39 | permanentAddress = ArrayList()
40 | placeOfBirth = ArrayList()
41 | tagPresenceList = ArrayList()
42 |
43 | this.custodyInformation = if (`in`.readInt() == 1) `in`.readString() else null
44 | this.fullDateOfBirth = if (`in`.readInt() == 1) `in`.readString() else null
45 | this.nameOfHolder = if (`in`.readInt() == 1) `in`.readString() else null
46 | if (`in`.readInt() == 1) {
47 | `in`.readList(otherNames!!, String::class.java.classLoader)
48 | }
49 | if (`in`.readInt() == 1) {
50 | `in`.readList(otherValidTDNumbers!!, String::class.java.classLoader)
51 | }
52 | if (`in`.readInt() == 1) {
53 | `in`.readList(permanentAddress!!, String::class.java.classLoader)
54 | }
55 | this.personalNumber = if (`in`.readInt() == 1) `in`.readString() else null
56 | this.personalSummary = if (`in`.readInt() == 1) `in`.readString() else null
57 | if (`in`.readInt() == 1) {
58 | `in`.readList(placeOfBirth!!, String::class.java.classLoader)
59 | }
60 | this.profession = if (`in`.readInt() == 1) `in`.readString() else null
61 | if (`in`.readInt() == 1) {
62 | this.proofOfCitizenship = ByteArray(`in`.readInt())
63 | `in`.readByteArray(this.proofOfCitizenship!!)
64 | }
65 | tag = `in`.readInt()
66 | if (`in`.readInt() == 1) {
67 | `in`.readList(tagPresenceList!!, Int::class.java.classLoader)
68 | }
69 |
70 | this.telephone = if (`in`.readInt() == 1) `in`.readString() else null
71 | this.title = if (`in`.readInt() == 1) `in`.readString() else null
72 |
73 |
74 | }
75 |
76 | override fun describeContents(): Int {
77 | return 0
78 | }
79 |
80 | override fun writeToParcel(dest: Parcel, flags: Int) {
81 | dest.writeInt(if (custodyInformation != null) 1 else 0)
82 | if (custodyInformation != null) {
83 | dest.writeString(custodyInformation)
84 | }
85 |
86 | dest.writeInt(if (fullDateOfBirth != null) 1 else 0)
87 | if (fullDateOfBirth != null) {
88 | dest.writeString(fullDateOfBirth)
89 | }
90 |
91 |
92 | dest.writeInt(if (nameOfHolder != null) 1 else 0)
93 | if (nameOfHolder != null) {
94 | dest.writeString(nameOfHolder)
95 | }
96 | dest.writeInt(if (otherNames != null) 1 else 0)
97 | if (otherNames != null) {
98 | dest.writeList(otherNames)
99 | }
100 |
101 | dest.writeInt(if (otherValidTDNumbers != null) 1 else 0)
102 | if (otherValidTDNumbers != null) {
103 | dest.writeList(otherValidTDNumbers)
104 | }
105 |
106 | dest.writeInt(if (permanentAddress != null) 1 else 0)
107 | if (permanentAddress != null) {
108 | dest.writeList(permanentAddress)
109 | }
110 |
111 | dest.writeInt(if (personalNumber != null) 1 else 0)
112 | if (personalNumber != null) {
113 | dest.writeString(personalNumber)
114 | }
115 |
116 | dest.writeInt(if (personalSummary != null) 1 else 0)
117 | if (personalSummary != null) {
118 | dest.writeString(personalSummary)
119 | }
120 |
121 | dest.writeInt(if (placeOfBirth != null) 1 else 0)
122 | if (placeOfBirth != null) {
123 | dest.writeList(placeOfBirth)
124 | }
125 |
126 | dest.writeInt(if (profession != null) 1 else 0)
127 | if (profession != null) {
128 | dest.writeString(profession)
129 | }
130 |
131 | dest.writeInt(if (proofOfCitizenship != null) 1 else 0)
132 | if (proofOfCitizenship != null) {
133 | dest.writeInt(proofOfCitizenship!!.size)
134 | dest.writeByteArray(proofOfCitizenship)
135 | }
136 |
137 | dest.writeInt(tag)
138 | dest.writeInt(if (tagPresenceList != null) 1 else 0)
139 | if (tagPresenceList != null) {
140 | dest.writeList(tagPresenceList)
141 | }
142 |
143 | dest.writeInt(if (telephone != null) 1 else 0)
144 | if (telephone != null) {
145 | dest.writeString(telephone)
146 | }
147 |
148 | dest.writeInt(if (title != null) 1 else 0)
149 | if (title != null) {
150 | dest.writeString(title)
151 | }
152 |
153 | }
154 |
155 | companion object {
156 | @JvmField
157 | val CREATOR: Parcelable.Creator<*> = object : Parcelable.Creator {
158 | override fun createFromParcel(pc: Parcel): AdditionalPersonDetails {
159 | return AdditionalPersonDetails(pc)
160 | }
161 |
162 | override fun newArray(size: Int): Array {
163 | return arrayOfNulls(size)
164 | }
165 | }
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/mlkit/GraphicOverlay.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package example.jllarraz.com.passportreader.mlkit
17 |
18 | import android.content.Context
19 | import android.graphics.Canvas
20 | import android.util.AttributeSet
21 | import android.view.View
22 |
23 |
24 | import java.util.HashSet
25 |
26 | /**
27 | * A view which renders a series of custom graphics to be overlayed on top of an associated preview
28 | * (i.e., the camera preview). The creator can add graphics objects, update the objects, and remove
29 | * them, triggering the appropriate drawing and invalidation within the view.
30 | *
31 | *
32 | * Supports scaling and mirroring of the graphics relative the camera's preview properties. The
33 | * idea is that detection items are expressed in terms of a preview size, but need to be scaled up
34 | * to the full view size, and also mirrored in the case of the front-facing camera.
35 | *
36 | *
37 | * Associated [Graphic] items should use the following methods to convert to view
38 | * coordinates for the graphics that are drawn:
39 | *
40 | *
41 | * 1. [Graphic.scaleX] and [Graphic.scaleY] adjust the size of the
42 | * supplied value from the preview scale to the view scale.
43 | * 1. [Graphic.translateX] and [Graphic.translateY] adjust the
44 | * coordinate from the preview's coordinate system to the view coordinate system.
45 | *
46 | */
47 | class GraphicOverlay(context: Context, attrs: AttributeSet) : View(context, attrs) {
48 | private val mLock = Any()
49 | private var mPreviewWidth: Int = 0
50 | private var mWidthScaleFactor = 1.0f
51 | private var mPreviewHeight: Int = 0
52 | private var mHeightScaleFactor = 1.0f
53 | private var mIsCameraFacing:Boolean = false
54 | private val mGraphics = HashSet()
55 |
56 | /**
57 | * Base class for a custom graphics object to be rendered within the graphic overlay. Subclass
58 | * this and implement the [Graphic.draw] method to define the
59 | * graphics element. Add instances to the overlay using [GraphicOverlay.add].
60 | */
61 | abstract class Graphic(private val mOverlay: GraphicOverlay) {
62 |
63 | /**
64 | * Draw the graphic on the supplied canvas. Drawing should use the following methods to
65 | * convert to view coordinates for the graphics that are drawn:
66 | *
67 | * 1. [Graphic.scaleX] and [Graphic.scaleY] adjust the size of
68 | * the supplied value from the preview scale to the view scale.
69 | * 1. [Graphic.translateX] and [Graphic.translateY] adjust the
70 | * coordinate from the preview's coordinate system to the view coordinate system.
71 | *
72 | *
73 | * @param canvas drawing canvas
74 | */
75 | abstract fun draw(canvas: Canvas)
76 |
77 | /**
78 | * Adjusts a horizontal value of the supplied value from the preview scale to the view
79 | * scale.
80 | */
81 | fun scaleX(horizontal: Float): Float {
82 | return horizontal * mOverlay.mWidthScaleFactor
83 | }
84 |
85 | /**
86 | * Adjusts a vertical value of the supplied value from the preview scale to the view scale.
87 | */
88 | fun scaleY(vertical: Float): Float {
89 | return vertical * mOverlay.mHeightScaleFactor
90 | }
91 |
92 | /**
93 | * Adjusts the x coordinate from the preview's coordinate system to the view coordinate
94 | * system.
95 | */
96 | fun translateX(x: Float): Float {
97 | return if (mOverlay.mIsCameraFacing == true) {
98 | mOverlay.width - scaleX(x)
99 | } else {
100 | scaleX(x)
101 | }
102 | }
103 |
104 | /**
105 | * Adjusts the y coordinate from the preview's coordinate system to the view coordinate
106 | * system.
107 | */
108 | fun translateY(y: Float): Float {
109 | return scaleY(y)
110 | }
111 |
112 | fun postInvalidate() {
113 | mOverlay.postInvalidate()
114 | }
115 | }
116 |
117 | /**
118 | * Removes all graphics from the overlay.
119 | */
120 | fun clear() {
121 | synchronized(mLock) {
122 | mGraphics.clear()
123 | }
124 | postInvalidate()
125 | }
126 |
127 | /**
128 | * Adds a graphic to the overlay.
129 | */
130 | fun add(graphic: Graphic) {
131 | synchronized(mLock) {
132 | mGraphics.add(graphic)
133 | }
134 | postInvalidate()
135 | }
136 |
137 | /**
138 | * Removes a graphic from the overlay.
139 | */
140 | fun remove(graphic: Graphic) {
141 | synchronized(mLock) {
142 | mGraphics.remove(graphic)
143 | }
144 | postInvalidate()
145 | }
146 |
147 | /**
148 | * Sets the camera attributes for size and facing direction, which informs how to transform
149 | * image coordinates later.
150 | */
151 | fun setCameraInfo(previewWidth: Int, previewHeight: Int, isCameraFacing: Boolean) {
152 | synchronized(mLock) {
153 | mPreviewWidth = previewWidth
154 | mPreviewHeight = previewHeight
155 | mIsCameraFacing = isCameraFacing
156 | }
157 | postInvalidate()
158 | }
159 |
160 | /**
161 | * Draws the overlay with its associated graphic objects.
162 | */
163 | override fun onDraw(canvas: Canvas) {
164 | super.onDraw(canvas)
165 |
166 | synchronized(mLock) {
167 | if (mPreviewWidth != 0 && mPreviewHeight != 0) {
168 | mWidthScaleFactor = canvas.width.toFloat() / mPreviewWidth.toFloat()
169 | mHeightScaleFactor = canvas.height.toFloat() / mPreviewHeight.toFloat()
170 | }
171 |
172 | for (graphic in mGraphics) {
173 | graphic.draw(canvas)
174 | }
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/utils/KeyStoreUtils.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.utils
2 |
3 | import android.net.Uri
4 | import android.util.Log
5 | import net.sf.scuba.data.Country
6 | import org.jmrtd.JMRTDSecurityProvider
7 | import org.spongycastle.jce.provider.BouncyCastleProvider
8 | import java.io.*
9 | import java.security.KeyStore
10 | import java.security.KeyStoreException
11 | import java.security.NoSuchAlgorithmException
12 | import java.security.Security
13 | import java.security.cert.*
14 | import java.util.*
15 | import javax.security.auth.x500.X500Principal
16 | import kotlin.collections.ArrayList
17 |
18 |
19 | class KeyStoreUtils {
20 |
21 |
22 | @Throws(KeyStoreException::class, NoSuchAlgorithmException::class, CertificateException::class, IOException::class)
23 | fun toKeyStore(certificates: Map): KeyStore? {
24 | val jmrtdProvIndex = JMRTDSecurityProvider.beginPreferBouncyCastleProvider()
25 | return try {
26 | val keyStore: KeyStore = KeyStore.getInstance("PKCS12")
27 | keyStore.load(null)
28 | for ((alias, certificate) in certificates) {
29 | println("DEBUG: adding certificate \"$alias\" to key store.")
30 | keyStore.setCertificateEntry(alias, certificate)
31 | }
32 | keyStore
33 | } finally {
34 | JMRTDSecurityProvider.endPreferBouncyCastleProvider(jmrtdProvIndex)
35 | }
36 | }
37 |
38 | @Throws(KeyStoreException::class, NoSuchAlgorithmException::class, CertificateException::class, IOException::class, IllegalStateException::class, IllegalArgumentException::class)
39 | fun toKeyStoreFile(certificates: Map, outputDir:File, fileName:String="csca.ks", password:String=""): Uri? {
40 | val toKeyStore = toKeyStore(certificates)
41 | /* Prepare output directory. */
42 |
43 | if (!outputDir.exists()) {
44 | Log.d("", "DEBUG: output dir " + outputDir.path.toString() + " doesn't exist, creating it.")
45 | if (!outputDir.mkdirs()) {
46 | throw IllegalStateException("Could not create output dir \"" + outputDir.path.toString() + "\"")
47 | }
48 | }
49 |
50 | if (!outputDir.isDirectory) {
51 | throw IllegalArgumentException("Output dir is not a directory")
52 | }
53 |
54 | /* Write to keystore. */
55 | val outFile = File(outputDir, fileName)
56 | val out = FileOutputStream(outFile)
57 | toKeyStore?.store(out, "".toCharArray())
58 |
59 | out.flush()
60 | out.close()
61 | return Uri.fromFile(outFile)
62 | }
63 |
64 | fun readKeystoreFromFile(folder:File, fileName:String="csca.ks", password:String=""):KeyStore?{
65 | try{
66 | val file = File(folder, fileName)
67 | val keyStore: KeyStore = KeyStore.getInstance("PKCS12")
68 | val fileInputStream = FileInputStream(file)
69 | keyStore.load(fileInputStream, password.toCharArray())
70 | return keyStore
71 | }catch (e:java.lang.Exception) {
72 | return null
73 | }
74 | }
75 |
76 | @Throws(CertificateEncodingException::class, IOException::class)
77 | fun toCertDir(certificates: Map, outputDir: String) {
78 | for ((alias, certificate) in certificates) {
79 | val outFile = File(outputDir, alias)
80 | val dataOut = DataOutputStream(FileOutputStream(outFile))
81 | dataOut.write(certificate.encoded)
82 | dataOut.close()
83 | }
84 | }
85 |
86 | fun getCountry(principal: X500Principal): Country? {
87 | val issuerName: String = principal.getName("RFC1779")
88 | val startIndex = issuerName.indexOf("C=")
89 | require(startIndex >= 0) { "Could not get country from issuer name, $issuerName" }
90 | var endIndex = issuerName.indexOf(",", startIndex)
91 | if (endIndex < 0) {
92 | endIndex = issuerName.length
93 | }
94 | val countryCode = issuerName.substring(startIndex + 2, endIndex).trim { it <= ' ' }.toUpperCase()
95 | return try {
96 | Country.getInstance(countryCode)
97 | } catch (e: Exception) {
98 | object : Country() {
99 | override fun valueOf(): Int {
100 | return -1
101 | }
102 |
103 | override fun getName(): String {
104 | return "Unknown country ($countryCode)"
105 | }
106 |
107 | override fun getNationality(): String {
108 | return "Unknown nationality ($countryCode)"
109 | }
110 |
111 | override fun toAlpha2Code(): String {
112 | return countryCode
113 | }
114 |
115 | override fun toAlpha3Code(): String {
116 | return "X$countryCode"
117 | }
118 | }
119 | }
120 | }
121 |
122 | fun toMap(certificates:List):Map{
123 | val treeMap = TreeMap()
124 | var i = 0
125 | for(certificate in certificates){
126 | val x509Certificate = certificate as X509Certificate
127 | val issuer = x509Certificate.getIssuerX500Principal()
128 | val subject = x509Certificate.getSubjectX500Principal()
129 | val serial = x509Certificate.getSerialNumber()
130 | val country = getCountry(issuer)
131 | val isSelfSigned = (issuer == null && subject == null) || subject.equals(issuer)
132 | val outName = country!!.toAlpha2Code().toLowerCase().toString() + "_" + (if (isSelfSigned) "root_" else "link_") + (++i) + ".cer"
133 | treeMap.put(outName, x509Certificate)
134 |
135 | }
136 | return treeMap
137 | }
138 | fun toList(keyStore: KeyStore):List{
139 | val aliases = keyStore.aliases()
140 | val list = ArrayList()
141 | for(alias in aliases) {
142 | val certificate = keyStore.getCertificate(alias)
143 | list.add(certificate)
144 | }
145 | return list
146 | }
147 |
148 | fun toCertStore(type:String="Collection", keyStore: KeyStore):CertStore{
149 | return CertStore.getInstance(type, CollectionCertStoreParameters(toList(keyStore)))
150 | }
151 |
152 | fun toAnchors(certificates: Collection): Set{
153 | val anchors = HashSet(certificates.size)
154 | for (certificate in certificates) {
155 | if (certificate is X509Certificate) {
156 | anchors.add(TrustAnchor(certificate, null))
157 | }
158 | }
159 | return anchors
160 | }
161 |
162 |
163 |
164 |
165 | companion object{
166 | init {
167 | Security.insertProviderAt(org.spongycastle.jce.provider.BouncyCastleProvider(), 1)
168 | }
169 | }
170 | }
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/utils/OcrUtils.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.utils
2 |
3 | import android.util.Log
4 | import com.google.mlkit.vision.text.Text
5 | import net.sf.scuba.data.Gender
6 | import org.jmrtd.lds.icao.MRZInfo
7 | import java.util.regex.Pattern
8 |
9 | object OcrUtils {
10 |
11 | private val TAG = OcrUtils::class.java.simpleName
12 |
13 | private val REGEX_OLD_PASSPORT = "(?[A-Z0-9<]{9})(?[0-9ILDSOG]{1})(?[A-Z<]{3})(?[0-9ILDSOG]{6})(?[0-9ILDSOG]{1})(?[FM<]){1}(?[0-9ILDSOG]{6})(?[0-9ILDSOG]{1})"
14 | private val REGEX_OLD_PASSPORT_CLEAN = "(?[A-Z0-9<]{9})(?[0-9]{1})(?[A-Z<]{3})(?[0-9]{6})(?[0-9]{1})(?[FM<]){1}(?[0-9]{6})(?[0-9]{1})"
15 | private val REGEX_IP_PASSPORT_LINE_1 = "\\bIP[A-Z<]{3}[A-Z0-9<]{9}[0-9]{1}"
16 | private val REGEX_IP_PASSPORT_LINE_2 = "[0-9]{6}[0-9]{1}[FM<]{1}[0-9]{6}[0-9]{1}[A-Z<]{3}"
17 |
18 | fun processOcr(
19 | results: Text,
20 | timeRequired: Long,
21 | callback: MRZCallback
22 | ){
23 | var fullRead = ""
24 | val blocks = results.textBlocks
25 | for (i in blocks.indices) {
26 | var temp = ""
27 | val lines = blocks[i].lines
28 | for (j in lines.indices) {
29 | //extract scanned text lines here
30 | //temp+=lines.get(j).getText().trim()+"-";
31 | temp += lines[j].text + "-"
32 | }
33 | temp = temp.replace("\r".toRegex(), "").replace("\n".toRegex(), "").replace("\t".toRegex(), "").replace(" ", "")
34 | fullRead += "$temp-"
35 | }
36 | fullRead = fullRead.toUpperCase()
37 | Log.d(TAG, "Read: $fullRead")
38 | val patternLineOldPassportType = Pattern.compile(REGEX_OLD_PASSPORT)
39 | val matcherLineOldPassportType = patternLineOldPassportType.matcher(fullRead)
40 |
41 |
42 |
43 | if (matcherLineOldPassportType.find()) {
44 | //Old passport format
45 | val line2 = matcherLineOldPassportType.group(0)
46 | var documentNumber = matcherLineOldPassportType.group(1)
47 | val checkDigitDocumentNumber = cleanDate(matcherLineOldPassportType.group(2)).toInt()
48 | val dateOfBirthDay = cleanDate(matcherLineOldPassportType.group(4))
49 | val expirationDate = cleanDate(matcherLineOldPassportType.group(7))
50 |
51 | val cleanDocumentNumber = cleanDocumentNumber(documentNumber, checkDigitDocumentNumber)
52 | if (cleanDocumentNumber!=null){
53 | val mrzInfo = createDummyMrz(documentNumber, dateOfBirthDay, expirationDate)
54 | callback.onMRZRead(mrzInfo, timeRequired)
55 | }else{
56 | //No success
57 | callback.onMRZReadFailure(timeRequired)
58 | }
59 |
60 |
61 | } else {
62 | //Try with the new IP passport type
63 | val patternLineIPassportTypeLine1 = Pattern.compile(REGEX_IP_PASSPORT_LINE_1)
64 | val matcherLineIPassportTypeLine1 = patternLineIPassportTypeLine1.matcher(fullRead)
65 | val patternLineIPassportTypeLine2 = Pattern.compile(REGEX_IP_PASSPORT_LINE_2)
66 | val matcherLineIPassportTypeLine2 = patternLineIPassportTypeLine2.matcher(fullRead)
67 | if (matcherLineIPassportTypeLine1.find() && matcherLineIPassportTypeLine2.find()) {
68 | val line1 = matcherLineIPassportTypeLine1.group(0)
69 | val line2 = matcherLineIPassportTypeLine2.group(0)
70 | var documentNumber = line1.substring(5, 14)
71 | val checkDigitDocumentNumber = line1.substring(14, 15).toInt()
72 | val dateOfBirthDay = line2.substring(0, 6)
73 | val expirationDate = line2.substring(8, 14)
74 |
75 | val cleanDocumentNumber = cleanDocumentNumber(documentNumber, checkDigitDocumentNumber)
76 | if (cleanDocumentNumber!=null){
77 | val mrzInfo = createDummyMrz(documentNumber, dateOfBirthDay, expirationDate)
78 | callback.onMRZRead(mrzInfo, timeRequired)
79 | }else{
80 | //No success
81 | callback.onMRZReadFailure(timeRequired)
82 | }
83 | } else {
84 | //No success
85 | callback.onMRZReadFailure(timeRequired)
86 | }
87 | }
88 | }
89 |
90 | private fun cleanDocumentNumber(documentNumber: String, checkDigit:Int):String?{
91 | //first we replace all O per 0
92 | var tempDcumentNumber = documentNumber.replace("O".toRegex(), "0")
93 | //Calculate check digit of the document number
94 | var checkDigitCalculated = MRZInfo.checkDigit(tempDcumentNumber).toString().toInt()
95 | if (checkDigit == checkDigitCalculated) {
96 | //If check digits match we return the document number
97 | return tempDcumentNumber
98 | }
99 | //if no match, we try to replace once at a time the first 0 per O as the alpha part comes first, and check if the digits match
100 | var indexOfZero = tempDcumentNumber.indexOf("0")
101 | while (indexOfZero>-1) {
102 | checkDigitCalculated = MRZInfo.checkDigit(tempDcumentNumber).toString().toInt()
103 | if (checkDigit != checkDigitCalculated) {
104 | //Some countries like Spain uses a letter O before the numeric part
105 | indexOfZero = tempDcumentNumber.indexOf("0")
106 | tempDcumentNumber = tempDcumentNumber.replaceFirst("0", "O")
107 | }else{
108 | return tempDcumentNumber
109 | }
110 | }
111 | return null
112 | }
113 |
114 | private fun createDummyMrz(documentNumber: String, dateOfBirthDay: String, expirationDate: String): MRZInfo {
115 | return MRZInfo(
116 | "P",
117 | "ESP",
118 | "DUMMY",
119 | "DUMMY",
120 | documentNumber,
121 | "ESP",
122 | dateOfBirthDay,
123 | Gender.MALE,
124 | expirationDate,
125 | ""
126 | )
127 | }
128 |
129 | private fun cleanDate(date:String):String{
130 | var tempDate = date
131 | tempDate = tempDate.replace("I".toRegex(), "1")
132 | tempDate = tempDate.replace("L".toRegex(), "1")
133 | tempDate = tempDate.replace("D".toRegex(), "0")
134 | tempDate = tempDate.replace("O".toRegex(), "0")
135 | tempDate = tempDate.replace("S".toRegex(), "5")
136 | tempDate = tempDate.replace("G".toRegex(), "6")
137 | return tempDate
138 | }
139 |
140 | interface MRZCallback {
141 | fun onMRZRead(mrzInfo: MRZInfo, timeRequired: Long)
142 | fun onMRZReadFailure(timeRequired: Long)
143 | fun onFailure(e: Exception, timeRequired: Long)
144 | }
145 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Passport Reader
3 |
4 | This device doesn\'t support Camera2 API.
5 |
6 | - Tesseract
7 | - Cube
8 | - Both
9 |
10 |
11 | Access to the camera is needed for detection
12 | This application cannot run because it does not have the camera permission. The application will now exit.
13 |
14 | Installing & Starting OCR
15 | Error Installing & Starting OCR
16 | Installation OCR: %1d%%
17 |
18 |
19 | Document Number: %1s Date of Birth: %2s Expiration Date: %3s
20 | OCR Detected - Time required: %2dms
21 | OCR Read failure - Time required: %1d ms
22 |
23 | Put your phone over your passport and don\'t move it
24 |
25 | Document Number
26 | Expiration Date
27 | Issuing Date
28 | Issuing State
29 | Nationality
30 | Passport
31 | NA
32 |
33 | %1s %2s
34 |
35 | Doc Number: %1s
36 | Date of Birth: %1s
37 | Expiry Date: %1s
38 |
39 | Data Entry
40 | Manual
41 | Automatic
42 |
43 | Document Number
44 | Document Expiration (yymmdd)
45 | Date of Birth (yymmdd)
46 |
47 | READ NFC
48 | Download Spanish CSCA Master List
49 | Delete CSCA Master List
50 |
51 |
52 | Date format is not valid
53 | Document number is not valid
54 |
55 | There is no NFC available
56 | You need to enable NFC
57 |
58 | Additional person information
59 | Custody
60 | Date of birth
61 | Other names
62 | Other Td numbers
63 | Permanent address
64 | Personal number
65 | Personal summary
66 | Place of birth
67 | Profession
68 | Telephone
69 | Title
70 |
71 | Authentication
72 | BAC
73 | PACE
74 | Chip
75 | Passive
76 | Active
77 | EAC
78 | Document Signing
79 | CSCA
80 |
81 |
82 | Additional document information
83 | Observations
84 | Date personalization
85 | Date issue
86 | Image front
87 | Image rear
88 | Issuing authority
89 | Names of other persons
90 | System serial number
91 | Tax or exit requirements
92 |
93 |
94 | Document Signing Certificate
95 | Country Signing Certificate
96 | Serial number
97 | Public key algorithm
98 | Signature algorithm
99 | Certificate thumbprint
100 | Issuer
101 | Subject
102 | Valid from
103 | Valid to
104 |
105 |
106 | Authentication has failed! Please try to scan the document again or introduce the data manually.
107 | Impossible to read the document. Passport doesn\'t support CLA .
108 |
109 | Valid passport
110 | Invalid Passport
111 | Unknown Passport
112 | The passport chip and content are valid
113 | The passport content is valid
114 | The passport chip is invalid
115 | The passport document information is invalid
116 | The CSCA information is invalid
117 | Unable to authenticate the passport
118 |
119 | Keystore
120 | Do you want to replace the current keystore?
121 |
122 |
123 |
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/ui/fragments/NfcFragment.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.ui.fragments
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.nfc.NfcAdapter
6 | import android.nfc.Tag
7 | import android.os.Bundle
8 | import android.os.Environment
9 | import android.os.Handler
10 | import android.os.Looper
11 | import android.util.Log
12 | import android.view.LayoutInflater
13 | import android.view.View
14 | import android.view.ViewGroup
15 | import android.widget.ProgressBar
16 | import android.widget.TextView
17 | import android.widget.Toast
18 |
19 | import net.sf.scuba.smartcards.CardServiceException
20 | import net.sf.scuba.smartcards.ISO7816
21 |
22 |
23 | import org.jmrtd.AccessDeniedException
24 | import org.jmrtd.BACDeniedException
25 | import org.jmrtd.PACEException
26 | import org.jmrtd.lds.icao.MRZInfo
27 |
28 |
29 | import java.security.Security
30 |
31 |
32 | import example.jllarraz.com.passportreader.R
33 | import example.jllarraz.com.passportreader.common.IntentData
34 | import example.jllarraz.com.passportreader.data.Passport
35 | import example.jllarraz.com.passportreader.databinding.FragmentNfcBinding
36 | import example.jllarraz.com.passportreader.utils.KeyStoreUtils
37 | import example.jllarraz.com.passportreader.utils.NFCDocumentTag
38 | import io.reactivex.disposables.CompositeDisposable
39 | import org.jmrtd.MRTDTrustStore
40 |
41 |
42 | class NfcFragment : androidx.fragment.app.Fragment() {
43 |
44 | private var mrzInfo: MRZInfo? = null
45 | private var nfcFragmentListener: NfcFragmentListener? = null
46 |
47 | internal var mHandler = Handler(Looper.getMainLooper())
48 | var disposable = CompositeDisposable()
49 |
50 | private var binding:FragmentNfcBinding?=null
51 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
52 | savedInstanceState: Bundle?): View? {
53 | binding = FragmentNfcBinding.inflate(inflater, container, false)
54 | return binding?.root
55 | }
56 |
57 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
58 | super.onViewCreated(view, savedInstanceState)
59 |
60 | val arguments = arguments
61 | if (arguments!!.containsKey(IntentData.KEY_MRZ_INFO)) {
62 | mrzInfo = arguments.getSerializable(IntentData.KEY_MRZ_INFO) as MRZInfo
63 | } else {
64 | //error
65 | }
66 | }
67 |
68 | fun handleNfcTag(intent: Intent?) {
69 | if (intent == null || intent.extras == null) {
70 | return
71 | }
72 | val tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) ?: return
73 |
74 | val folder = requireContext().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!
75 | val keyStore = KeyStoreUtils().readKeystoreFromFile(folder)
76 |
77 | val mrtdTrustStore = MRTDTrustStore()
78 | if(keyStore!=null){
79 | val certStore = KeyStoreUtils().toCertStore(keyStore = keyStore)
80 |
81 | mrtdTrustStore.addAsCSCACertStore(certStore)
82 | }
83 | //mrtdTrustStore.addCSCAStore(readKeystoreFromFile)
84 |
85 |
86 | val subscribe = NFCDocumentTag().handleTag(requireContext(), tag, mrzInfo!!, mrtdTrustStore, object : NFCDocumentTag.PassportCallback {
87 |
88 | override fun onPassportReadStart() {
89 | onNFCSReadStart()
90 | }
91 |
92 | override fun onPassportReadFinish() {
93 | onNFCReadFinish()
94 | }
95 |
96 | override fun onPassportRead(passport: Passport?) {
97 | this@NfcFragment.onPassportRead(passport)
98 |
99 | }
100 |
101 | override fun onAccessDeniedException(exception: AccessDeniedException) {
102 | Toast.makeText(context, getString(R.string.warning_authentication_failed), Toast.LENGTH_SHORT).show()
103 | exception.printStackTrace()
104 | this@NfcFragment.onCardException(exception)
105 |
106 | }
107 |
108 | override fun onBACDeniedException(exception: BACDeniedException) {
109 | Toast.makeText(context, exception.toString(), Toast.LENGTH_SHORT).show()
110 | this@NfcFragment.onCardException(exception)
111 | }
112 |
113 | override fun onPACEException(exception: PACEException) {
114 | Toast.makeText(context, exception.toString(), Toast.LENGTH_SHORT).show()
115 | this@NfcFragment.onCardException(exception)
116 | }
117 |
118 | override fun onCardException(exception: CardServiceException) {
119 | val sw = exception.sw.toShort()
120 | when (sw) {
121 | ISO7816.SW_CLA_NOT_SUPPORTED -> {
122 | Toast.makeText(context, getString(R.string.warning_cla_not_supported), Toast.LENGTH_SHORT).show()
123 | }
124 | else -> {
125 | Toast.makeText(context, exception.toString(), Toast.LENGTH_SHORT).show()
126 | }
127 | }
128 | this@NfcFragment.onCardException(exception)
129 | }
130 |
131 | override fun onGeneralException(exception: Exception?) {
132 | Toast.makeText(context, exception!!.toString(), Toast.LENGTH_SHORT).show()
133 | this@NfcFragment.onCardException(exception)
134 | }
135 | })
136 |
137 | disposable.add(subscribe)
138 |
139 | }
140 |
141 | override fun onAttach(context: Context) {
142 | super.onAttach(context)
143 | val activity = activity
144 | if (activity is NfcFragment.NfcFragmentListener) {
145 | nfcFragmentListener = activity
146 | }
147 | }
148 |
149 | override fun onDetach() {
150 | nfcFragmentListener = null
151 | super.onDetach()
152 | }
153 |
154 |
155 | override fun onResume() {
156 | super.onResume()
157 |
158 | binding?.valuePassportNumber?.text = getString(R.string.doc_number, mrzInfo!!.documentNumber)
159 | binding?.valueDOB?.text = getString(R.string.doc_dob, mrzInfo!!.dateOfBirth)
160 | binding?.valueExpirationDate?.text = getString(R.string.doc_expiry, mrzInfo!!.dateOfExpiry)
161 |
162 | if (nfcFragmentListener != null) {
163 | nfcFragmentListener!!.onEnableNfc()
164 | }
165 | }
166 |
167 | override fun onPause() {
168 | super.onPause()
169 | if (nfcFragmentListener != null) {
170 | nfcFragmentListener!!.onDisableNfc()
171 | }
172 | }
173 |
174 | override fun onDestroyView() {
175 | if (!disposable.isDisposed()) {
176 | disposable.dispose();
177 | }
178 | binding = null
179 | super.onDestroyView()
180 | }
181 |
182 | protected fun onNFCSReadStart() {
183 | Log.d(TAG, "onNFCSReadStart")
184 | mHandler.post {
185 | binding?.progressBar?.visibility = View.VISIBLE }
186 |
187 | }
188 |
189 | protected fun onNFCReadFinish() {
190 | Log.d(TAG, "onNFCReadFinish")
191 | mHandler.post { binding?.progressBar?.visibility = View.GONE }
192 | }
193 |
194 | protected fun onCardException(cardException: Exception?) {
195 | mHandler.post {
196 | if (nfcFragmentListener != null) {
197 | nfcFragmentListener?.onCardException(cardException)
198 | }
199 | }
200 | }
201 |
202 | protected fun onPassportRead(passport: Passport?) {
203 | mHandler.post {
204 | if (nfcFragmentListener != null) {
205 | nfcFragmentListener?.onPassportRead(passport)
206 | }
207 | }
208 | }
209 |
210 | interface NfcFragmentListener {
211 | fun onEnableNfc()
212 | fun onDisableNfc()
213 | fun onPassportRead(passport: Passport?)
214 | fun onCardException(cardException: Exception?)
215 | }
216 |
217 |
218 |
219 | companion object {
220 | private val TAG = NfcFragment::class.java.simpleName
221 |
222 | init {
223 | Security.insertProviderAt(org.spongycastle.jce.provider.BouncyCastleProvider(), 1)
224 | }
225 | fun newInstance(mrzInfo: MRZInfo): NfcFragment {
226 | val myFragment = NfcFragment()
227 | val args = Bundle()
228 | args.putSerializable(IntentData.KEY_MRZ_INFO, mrzInfo)
229 | myFragment.arguments = args
230 | return myFragment
231 | }
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/app/src/main/java/org/jmrtd/cert/CSCAMasterList.kt:
--------------------------------------------------------------------------------
1 | package org.jmrtd.cert
2 |
3 |
4 | import org.spongycastle.asn1.*
5 | import org.spongycastle.asn1.pkcs.SignedData
6 | import org.spongycastle.jce.provider.X509CertificateObject
7 | import java.io.ByteArrayInputStream
8 | import java.io.IOException
9 | import java.security.cert.CertSelector
10 | import java.security.cert.Certificate
11 | import java.security.cert.X509CertSelector
12 | import java.security.cert.X509Certificate
13 | import java.util.*
14 |
15 | class CSCAMasterList private constructor() {
16 | private val certificates: MutableList
17 |
18 | /**
19 | * Constructs a master lsit from a collection of certificates.
20 | *
21 | * @param certificates a collection of certificates
22 | */
23 | constructor(certificates: Collection?) : this() {
24 | this.certificates.addAll(certificates!!)
25 | }
26 |
27 | @JvmOverloads
28 | constructor(binary: ByteArray, selector: CertSelector = IDENTITY_SELECTOR) : this() {
29 | certificates.addAll(searchCertificates(binary, selector))
30 | }
31 |
32 | fun getCertificates(): List {
33 | return certificates
34 | }
35 |
36 | companion object {
37 | /** Use this to get all certificates, including link certificates. */
38 | private val IDENTITY_SELECTOR: CertSelector = object : X509CertSelector() {
39 | override fun match(cert: Certificate): Boolean {
40 | return if (cert !is X509Certificate) {
41 | false
42 | } else true
43 | }
44 |
45 | override fun clone(): Any {
46 | return this
47 | }
48 | }
49 |
50 | /** Use this to get self-signed certificates only. (Excludes link certificates.) */
51 | private val SELF_SIGNED_SELECTOR: CertSelector = object : X509CertSelector() {
52 | override fun match(cert: Certificate): Boolean {
53 | if (cert !is X509Certificate) {
54 | return false
55 | }
56 | val x509Cert = cert
57 | val issuer = x509Cert.issuerX500Principal
58 | val subject = x509Cert.subjectX500Principal
59 | return issuer == null && subject == null || subject == issuer
60 | }
61 |
62 | override fun clone(): Any {
63 | return this
64 | }
65 | }
66 |
67 | /* PRIVATE METHODS BELOW */
68 | private fun searchCertificates(binary: ByteArray, selector: CertSelector): List {
69 | val result: MutableList = ArrayList()
70 | try {
71 | val sequence = ASN1Sequence.getInstance(binary) as ASN1Sequence
72 | val signedDataList: List? = getSignedDataFromDERObject(sequence, null)
73 | for (signedData in signedDataList!!) {
74 |
75 | // ASN1Set certificatesASN1Set = signedData.getCertificates();
76 | // Enumeration certificatesEnum = certificatesASN1Set.getObjects();
77 | // while (certificatesEnum.hasMoreElements()) {
78 | // Object certificateObject = certificatesEnum.nextElement();
79 | // // TODO: interpret certificateObject, and check signature
80 | // }
81 | val contentInfo = signedData.contentInfo
82 | val content: Any = contentInfo.content
83 | val certificates: Collection? = getCertificatesFromDERObject(content, null)
84 | for (certificate in certificates!!) {
85 | if (selector.match(certificate)) {
86 | result.add(certificate)
87 | }
88 | }
89 | }
90 | } catch (e: Exception) {
91 | e.printStackTrace()
92 | }
93 | return result
94 | }
95 |
96 | private fun getSignedDataFromDERObject(o: Any, result: MutableList?): MutableList? {
97 | var result = result
98 | if (result == null) {
99 | result = ArrayList()
100 | }
101 | try {
102 | val signedData = SignedData.getInstance(o)
103 | if (signedData != null) {
104 | result.add(signedData)
105 | }
106 | return result
107 | } catch (e: Exception) {
108 | }
109 | if (o is DERTaggedObject) {
110 | val childObject = o.getObject()
111 | return getSignedDataFromDERObject(childObject, result)
112 | } else if (o is ASN1Sequence) {
113 | val derObjects = o.objects
114 | while (derObjects.hasMoreElements()) {
115 | val nextObject = derObjects.nextElement()
116 | result = getSignedDataFromDERObject(nextObject, result)
117 | }
118 | return result
119 | } else if (o is ASN1Set) {
120 | val derObjects = o.objects
121 | while (derObjects.hasMoreElements()) {
122 | val nextObject = derObjects.nextElement()
123 | result = getSignedDataFromDERObject(nextObject, result)
124 | }
125 | return result
126 | } else if (o is DEROctetString) {
127 | val octets = o.octets
128 | val derInputStream = ASN1InputStream(ByteArrayInputStream(octets))
129 | try {
130 | while (true) {
131 | val derObject = derInputStream.readObject() ?: break
132 | result = getSignedDataFromDERObject(derObject, result)
133 | }
134 | derInputStream.close()
135 | } catch (ioe: IOException) {
136 | ioe.printStackTrace()
137 | }
138 | return result
139 | }
140 | return result
141 | }
142 |
143 | private fun getCertificatesFromDERObject(o: Any, certificates: MutableCollection?): MutableCollection? {
144 | var certificates = certificates
145 | if (certificates == null) {
146 | certificates = ArrayList()
147 | }
148 | try {
149 | val certAsASN1Object = org.spongycastle.asn1.x509.Certificate.getInstance(o)
150 | certificates.add(X509CertificateObject(certAsASN1Object)) // NOTE: >= BC 1.48
151 | // certificates.add(new X509CertificateObject(X509CertificateStructure.getInstance(certAsASN1Object))); // NOTE: <= BC 1.47
152 | return certificates
153 | } catch (e: Exception) {
154 | }
155 | if (o is DERTaggedObject) {
156 | val childObject = o.getObject()
157 | return getCertificatesFromDERObject(childObject, certificates)
158 | } else if (o is ASN1Sequence) {
159 | val derObjects = o.objects
160 | while (derObjects.hasMoreElements()) {
161 | val nextObject = derObjects.nextElement()
162 | certificates = getCertificatesFromDERObject(nextObject, certificates)
163 | }
164 | return certificates
165 | } else if (o is ASN1Set) {
166 | val derObjects = o.objects
167 | while (derObjects.hasMoreElements()) {
168 | val nextObject = derObjects.nextElement()
169 | certificates = getCertificatesFromDERObject(nextObject, certificates)
170 | }
171 | return certificates
172 | } else if (o is DEROctetString) {
173 | val octets = o.octets
174 | val derInputStream = ASN1InputStream(ByteArrayInputStream(octets))
175 | try {
176 | while (true) {
177 | val derObject = derInputStream.readObject() ?: break
178 | certificates = getCertificatesFromDERObject(derObject, certificates)
179 | }
180 | } catch (ioe: IOException) {
181 | ioe.printStackTrace()
182 | }
183 | return certificates
184 | } else if (o is SignedData) {
185 | // ASN1Set certificatesASN1Set = signedData.getCertificates();
186 | // Enumeration certificatesEnum = certificatesASN1Set.getObjects();
187 | // while (certificatesEnum.hasMoreElements()) {
188 | // Object certificateObject = certificatesEnum.nextElement();
189 | // // TODO: interpret certificateObject, and check signature
190 | // }
191 | val contentInfo = o.contentInfo
192 | val content: Any = contentInfo.content
193 | return getCertificatesFromDERObject(content, certificates)
194 | }
195 | return certificates
196 | }
197 | }
198 |
199 | /** Private constructor, only used locally. */
200 | init {
201 | certificates = ArrayList(256)
202 | }
203 | }
--------------------------------------------------------------------------------
/app/src/main/java/example/jllarraz/com/passportreader/utils/PassportNfcUtils.kt:
--------------------------------------------------------------------------------
1 | package example.jllarraz.com.passportreader.utils
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.util.Log
6 |
7 | import org.jmrtd.cert.CVCPrincipal
8 | import org.jmrtd.cert.CardVerifiableCertificate
9 | import org.jmrtd.lds.icao.DG2File
10 | import org.jmrtd.lds.icao.DG3File
11 | import org.jmrtd.lds.icao.DG5File
12 | import org.jmrtd.lds.icao.DG7File
13 | import org.jmrtd.lds.iso19794.FaceImageInfo
14 | import org.jmrtd.lds.iso19794.FingerImageInfo
15 | import org.spongycastle.jce.provider.BouncyCastleProvider
16 |
17 | import java.io.ByteArrayInputStream
18 | import java.io.DataInputStream
19 | import java.io.IOException
20 | import java.io.InputStream
21 |
22 | import java.math.BigInteger
23 | import java.security.GeneralSecurityException
24 | import java.security.KeyStore
25 | import java.security.PrivateKey
26 | import java.security.Security
27 | import java.security.cert.CertPathBuilder
28 | import java.security.cert.CertPathBuilderException
29 | import java.security.cert.CertStore
30 | import java.security.cert.Certificate
31 | import java.security.cert.CollectionCertStoreParameters
32 | import java.security.cert.PKIXBuilderParameters
33 | import java.security.cert.PKIXCertPathBuilderResult
34 | import java.security.cert.TrustAnchor
35 | import java.security.cert.X509CertSelector
36 | import java.security.cert.X509Certificate
37 | import java.util.ArrayList
38 | import java.util.Arrays
39 | import java.util.Collections
40 |
41 | import javax.security.auth.x500.X500Principal
42 |
43 | object PassportNfcUtils {
44 |
45 | private val TAG = PassportNfcUtils::class.java.simpleName
46 |
47 |
48 | private val IS_PKIX_REVOCATION_CHECING_ENABLED = false
49 |
50 | init {
51 | Security.addProvider(BouncyCastleProvider())
52 | }
53 |
54 |
55 | @Throws(IOException::class)
56 | fun retrieveFaceImage(context: Context, dg2File: DG2File): Bitmap {
57 | val allFaceImageInfos = ArrayList()
58 | val faceInfos = dg2File.faceInfos
59 | for (faceInfo in faceInfos) {
60 | allFaceImageInfos.addAll(faceInfo.faceImageInfos)
61 | }
62 |
63 | if (!allFaceImageInfos.isEmpty()) {
64 | val faceImageInfo = allFaceImageInfos.iterator().next()
65 | return toBitmap(faceImageInfo.imageLength, faceImageInfo.imageInputStream, faceImageInfo.mimeType)
66 | }
67 | throw IOException("Unable to decodeImage Image")
68 | }
69 |
70 | @Throws(IOException::class)
71 | fun retrievePortraitImage(context: Context, dg5File: DG5File): Bitmap {
72 | val faceInfos = dg5File.images
73 | if (!faceInfos.isEmpty()) {
74 | val faceImageInfo = faceInfos.iterator().next()
75 | return toBitmap(faceImageInfo.imageLength, faceImageInfo.imageInputStream, faceImageInfo.mimeType)
76 | }
77 | throw IOException("Unable to decodeImage Image")
78 | }
79 |
80 | @Throws(IOException::class)
81 | fun retrieveSignatureImage(context: Context, dg7File: DG7File): Bitmap {
82 | val displayedImageInfos = dg7File.images
83 | if (!displayedImageInfos.isEmpty()) {
84 | val displayedImageInfo = displayedImageInfos.iterator().next()
85 | return toBitmap(displayedImageInfo.imageLength, displayedImageInfo.imageInputStream, displayedImageInfo.mimeType)
86 | }
87 | throw IOException("Unable to decodeImage Image")
88 | }
89 |
90 | @Throws(IOException::class)
91 | fun retrieveFingerPrintImage(context: Context, dg3File: DG3File): List {
92 | val allFingerImageInfos = ArrayList()
93 | val fingerInfos = dg3File.fingerInfos
94 |
95 | val fingerprintsImage = ArrayList()
96 | for (fingerInfo in fingerInfos) {
97 | allFingerImageInfos.addAll(fingerInfo.fingerImageInfos)
98 | }
99 |
100 | val iterator = allFingerImageInfos.iterator()
101 | while (iterator.hasNext()) {
102 | val fingerImageInfo = iterator.next()
103 | val bitmap = toBitmap(fingerImageInfo.imageLength, fingerImageInfo.imageInputStream, fingerImageInfo.mimeType)
104 | fingerprintsImage.add(bitmap)
105 | }
106 |
107 | if (fingerprintsImage.isEmpty()) {
108 | throw IOException("Unable to decodeImage Finger print Image")
109 | }
110 | return fingerprintsImage
111 |
112 | }
113 |
114 |
115 | @Throws(IOException::class)
116 | private fun toBitmap(imageLength: Int, inputStream: InputStream, mimeType: String): Bitmap {
117 | val dataInputStream = DataInputStream(inputStream)
118 | val buffer = ByteArray(imageLength)
119 | dataInputStream.readFully(buffer, 0, imageLength)
120 | val byteArrayInputStream = ByteArrayInputStream(buffer, 0, imageLength)
121 | return ImageUtil.decodeImage(byteArrayInputStream, imageLength, mimeType)
122 | }
123 |
124 |
125 | @Throws(GeneralSecurityException::class)
126 | fun getEACCredentials(caReference: CVCPrincipal, cvcaStores: List): EACCredentials? {
127 | for (cvcaStore in cvcaStores) {
128 | val eacCredentials = getEACCredentials(caReference, cvcaStore)
129 | if (eacCredentials != null) {
130 | return eacCredentials
131 | }
132 | }
133 | return null
134 | }
135 |
136 | /**
137 | * Searches the key store for a relevant terminal key and associated certificate chain.
138 | *
139 | * @param caReference
140 | * @param cvcaStore should contain a single key with certificate chain
141 | * @return
142 | * @throws GeneralSecurityException
143 | */
144 | @Throws(GeneralSecurityException::class)
145 | private fun getEACCredentials(caReference: CVCPrincipal?, cvcaStore: KeyStore): EACCredentials? {
146 | if (caReference == null) {
147 | throw IllegalArgumentException("CA reference cannot be null")
148 | }
149 |
150 | var privateKey: PrivateKey? = null
151 | var chain: Array? = null
152 |
153 | val aliases = Collections.list(cvcaStore.aliases())
154 | for (alias in aliases) {
155 | if (cvcaStore.isKeyEntry(alias)) {
156 | val key = cvcaStore.getKey(alias, "".toCharArray())
157 | if (key is PrivateKey) {
158 | privateKey = key
159 | } else {
160 | Log.w(TAG, "skipping non-private key $alias")
161 | continue
162 | }
163 | chain = cvcaStore.getCertificateChain(alias)
164 | return EACCredentials(privateKey, chain!!)
165 | } else if (cvcaStore.isCertificateEntry(alias)) {
166 | val certificate = cvcaStore.getCertificate(alias) as CardVerifiableCertificate
167 | val authRef = certificate.authorityReference
168 | val holderRef = certificate.holderReference
169 | if (caReference != authRef) {
170 | continue
171 | }
172 | /* See if we have a private key for that certificate. */
173 | privateKey = cvcaStore.getKey(holderRef.name, "".toCharArray()) as PrivateKey
174 | chain = cvcaStore.getCertificateChain(holderRef.name)
175 | if (privateKey == null) {
176 | continue
177 | }
178 | Log.i(TAG, "found a key, privateKey = $privateKey")
179 | return EACCredentials(privateKey, chain!!)
180 | }
181 | if (privateKey == null || chain == null) {
182 | Log.e(TAG, "null chain or key for entry " + alias + ": chain = " + Arrays.toString(chain) + ", privateKey = " + privateKey)
183 | continue
184 | }
185 | }
186 | return null
187 | }
188 |
189 | /**
190 | * Builds a certificate chain to an anchor using the PKIX algorithm.
191 | *
192 | * @param docSigningCertificate the start certificate
193 | * @param sodIssuer the issuer of the start certificate (ignored unless `docSigningCertificate` is `null`)
194 | * @param sodSerialNumber the serial number of the start certificate (ignored unless `docSigningCertificate` is `null`)
195 | *
196 | * @return the certificate chain
197 | */
198 | fun getCertificateChain(docSigningCertificate: X509Certificate?,
199 | sodIssuer: X500Principal,
200 | sodSerialNumber: BigInteger,
201 | cscaStores: List,
202 | cscaTrustAnchors: Set): List {
203 | val chain = ArrayList()
204 | val selector = X509CertSelector()
205 | try {
206 |
207 | if (docSigningCertificate != null) {
208 | selector.certificate = docSigningCertificate
209 | } else {
210 | selector.issuer = sodIssuer
211 | selector.serialNumber = sodSerialNumber
212 | }
213 |
214 | val docStoreParams = CollectionCertStoreParameters(setOf(docSigningCertificate as Certificate))
215 | val docStore = CertStore.getInstance("Collection", docStoreParams)
216 |
217 | val builder = CertPathBuilder.getInstance("PKIX", "SC")//Spungy castle
218 | val buildParams = PKIXBuilderParameters(cscaTrustAnchors, selector)
219 | buildParams.addCertStore(docStore)
220 | for (trustStore in cscaStores) {
221 | buildParams.addCertStore(trustStore)
222 | }
223 | buildParams.isRevocationEnabled = IS_PKIX_REVOCATION_CHECING_ENABLED /* NOTE: set to false for checking disabled. */
224 |
225 | var result: PKIXCertPathBuilderResult? = null
226 |
227 | try {
228 | result = builder.build(buildParams) as PKIXCertPathBuilderResult
229 | } catch (cpbe: CertPathBuilderException) {
230 | cpbe.printStackTrace()
231 | /* NOTE: ignore, result remain null */
232 | }
233 |
234 | if (result != null) {
235 | val pkixCertPath = result.certPath
236 | if (pkixCertPath != null) {
237 | chain.addAll(pkixCertPath.certificates)
238 | }
239 | }
240 | if (docSigningCertificate != null && !chain.contains(docSigningCertificate)) {
241 | /* NOTE: if doc signing certificate not in list, we add it ourselves. */
242 | Log.w(TAG, "Adding doc signing certificate after PKIXBuilder finished")
243 | chain.add(0, docSigningCertificate)
244 | }
245 | if (result != null) {
246 | val trustAnchorCertificate = result.trustAnchor.trustedCert
247 | if (trustAnchorCertificate != null && !chain.contains(trustAnchorCertificate)) {
248 | /* NOTE: if trust anchor not in list, we add it ourselves. */
249 | Log.w(TAG, "Adding trust anchor certificate after PKIXBuilder finished")
250 | chain.add(trustAnchorCertificate)
251 | }
252 | }
253 | } catch (e: Exception) {
254 | e.printStackTrace()
255 | Log.i(TAG, "Building a chain failed (" + e.message + ").")
256 | }
257 |
258 | return chain
259 | }
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 | }
274 |
--------------------------------------------------------------------------------