├── 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 | 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 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 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 | ![alt text](https://github.com/jllarraz/AndroidPassportReader/blob/master/examples/passport_ireland.jpg) 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 | 24 | 41 | 42 | 43 | 44 | 45 | 46 | 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 | 6 | 7 | 8 | 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 | 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 |