-
CameraX api 5-2편 : OCR + 여권(MRZ + NFC)회사 생활/여권 NFC (CameraX + OCR + NFC) 2023. 11. 25. 15:30
4편에서 OCR과 5편에서 MRZ부분을 다뤘기 때문에 NFC부분만 다루겠다.
우선 먼저 안드로이드에서 여권 NFC를 하기위해 필요한 라이브러리 부터 불러온다.
build.gradle(app)
implementation 'org.jmrtd:jmrtd:0.7.18' implementation 'net.sf.scuba:scuba-sc-android:0.0.18' implementation 'com.madgag.spongycastle:prov:1.58.0.0' implementation 'edu.ucar:jj2000:5.2' implementation 'com.github.mhshams:jnbis:1.1.0'
그리고 NFC를 하기위해 필요한 여권번호, 생년월인, 여권만료일자를 가져온다.
val intent: Intent = getIntent() mrzInfo = (intent.getSerializableExtra(MRZ_RESULT) as MRZInfo?)!!
OCR을 통해 CameraX를 이용해 여권 MRZ 코드로 부터 가져온 데이터를 MRZInfo에 넣어주고 MRZInfo를 Intent로 다른 Activity에 넘겨 주었다.
MRZInfo class build.gradle에 'implementation 'org.jmrtd:jmrtd:0.7.18' 가 추가 되었을때 같이 추가된다. Serializable를 상속해서 Intent로 넘겨줄수 있게 되어있다. (대충 아래와 같은 필드를 갖고 있다.)
더보기private int documentType; private String documentCode; private String issuingState; private String primaryIdentifier; private String secondaryIdentifier; private String nationality; private String documentNumber; private String dateOfBirth; private Gender gender; private String dateOfExpiry; private char documentNumberCheckDigit; private char dateOfBirthCheckDigit; private char dateOfExpiryCheckDigit; private char compositeCheckDigit; private String optionalData1; /* NOTE: holds personal number for some issuing states (e.g. NL), but is used to hold (part of) document number for others. */ private String optionalData2;
그 다음 NFC를 사용하도록 설정해 준다. NFC는 application class에 설정해 줬다.
class MainApplication : Application() { private lateinit var adapter: NfcAdapter ... 생략 ... override fun onCreate() { super.onCreate() adapter = NfcAdapter.getDefaultAdapter(this) } ... 생략 ... fun getNFCAdapter(): NfcAdapter { return adapter } //NFC 진동이 작동되는거 자체를 막음 fun stopNFCReader(mActivity: Activity) { if (adapter != null) adapter.enableReaderMode(mActivity, null, NfcAdapter.STATE_TURNING_OFF, null); } }
NFC를 사용하기 위해서는 NFCAdapter를 불러와야한다.
NFCAdapter를 NFC에 사용되는 곳이 아닌 Application class에 만들어준 이유는 NFC를 활성화 시키고 나서 NFC reading이 되는 것을 막고 싶어도 안막아졌다.
(이부분은 아래에서 NFC를 활성화 시키면서 한번 더 언급하겠다. 참고1, 참고2)
그래서 각 Activity 별로 저기 있는 stopNFCReader()를 사용해서 정지시켜주었고, 모든 Activity마다 추가 시킬 경우 코드의 중복이 많아져서 저렇게 했다.
실제 NFC를 진행하는 Activity를 제외한 모든 Activity에 아래와 같이 stopNFCReader()를 불러와 NFC가 읽혀지는 것을 방지했다. (그렇게 하지 않으면 여권과 가까이 있을 경우 진동이 느껴진 후 NFC를 사용하는 다른 앱이 켜졌다.)
override fun onStart() { super.onStart() (application as MainApplication).stopNFCReader(this) }
이제 NFC를 활성화 시켜주는 부분이다. 이부분은 NFC를 활용하는 Activity에서 해주면 된다.
override fun onResume() { super.onResume() Log.d("123123123", "NfcScanActivity - onResume") if (adapter == null) { // 기기가 NFC를 지원하지 않음. Toast.makeText(this, "NFC 기능이 없어서 사용할 수가 없습니다..", Toast.LENGTH_SHORT).show() } else if (!adapter.isEnabled) { // NFC가 비활성화되어 있음 val intent = Intent(android.provider.Settings.ACTION_NFC_SETTINGS) startActivity(intent) } else { // NFC가 켜져있음 val pendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE) val filter: Array<Array<String>> = arrayOf(arrayOf("android.nfc.tech.IsoDep")) adapter.enableForegroundDispatch(this, pendingIntent, null, filter) } }
override fun onPause() { super.onPause() if (adapter != null) adapter.disableForegroundDispatch(this) }
스마트폰 기종에 따라서 NFC칩이 없는 경우도 있고, NFC가 꺼져있는 경우도 있다. 그래서 그 부분을 나눠서 구현했다.
adapter.enableForegroundDispatch(this, pendingIntent, null, filter
enableForegroundDispatch를 사용해서 NFC를 활성화 시켜준다.
PendingIntent는 목적은 다른 애플리케이션의 권한을 허용하여 가지고 있는 Intent를 본인 앱에서 실행할 수 있게 해주는 것이다.
filter를 이용해서 여러 NFC 이벤트중 "android.nfc.tech.IsoDep" 의 종류만 처리한다.
더보기<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <tech-list> <tech>android.nfc.tech.IsoDep</tech> <tech>android.nfc.tech.NfcA</tech> <tech>android.nfc.tech.NfcB</tech> <tech>android.nfc.tech.NfcF</tech> <tech>android.nfc.tech.NfcV</tech> <tech>android.nfc.tech.Ndef</tech> <tech>android.nfc.tech.NdefFormatable</tech> <tech>android.nfc.tech.MifareClassic</tech> <tech>android.nfc.tech.MifareUltralight</tech> </tech-list> </resources>
공부해보면 재미있을 것 같기는 한데 시간이 없어서 생략......
가장 의문인 부분은 disableForegroundDispatch 부분이다. 이것을 onPause에 추가시킨다고 해도 NFC가 비활성화가 되는 것이 아니었다!!!
그래서 앞서 얘기했듯이 Application class에 정의해둔 stopNFCReader를 활용해서 다른 Activity에서는 NFC가 안되게 막아두었다.
스마트폰이 NFC관련 데이터가 넘어올 경우 onNewIntent로 넘어온다.
override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action) { val tag: Tag? = intent.extras?.getParcelable(NfcAdapter.EXTRA_TAG) if (tag != null) { if (listOf(*tag.techList).contains("android.nfc.tech.IsoDep") && application.nfcIsRunning) { if (((passportNumber != "") && !passportNumber.isEmpty() && (expirationDate != "") && !expirationDate.isEmpty() && (birthDate != "") && !birthDate.isEmpty()) ) { val bacKey: BACKeySpec = BACKey(passportNumber, birthDate, expirationDate) ReadTask(IsoDep.get(tag), bacKey, this, adapter, this).execute() } else { Log.d("onPostExecute", "onPostExecute444") } } else { Log.d("onPostExecute", "onPostExecute333") } } } else { Log.d("onPostExecute", "onPostExecute222") } }
여권번호, 만료일자, 생일 정보가 비어있지 않다면 background에서 NFC로부터 정보를 읽어오는 코드가 실행된다.
다른 예제들을 보니 AsyncTask로 구현되어 있었다. 하지만 AsyncTask는 deprecated 되서 그것을 대체하는 것까지 찾아서 구현해주었다. (이부분은 좀더 공부할 필요성이 있을 것 같다.)
NFC한 데이터를 불러오는 코드이다
더보기class ReadTask constructor( private val isoDep: IsoDep, private val bacKey: BACKeySpec, private val mContext: Context, ) : ThreadTask<Void?, Void?, Exception?>() { private var eDocument: EDocument = EDocument() private var personDetails: PersonDetails = PersonDetails() private var additionalPersonDetails: AdditionalPersonDetails = AdditionalPersonDetails() private var additionalDocumentDetails: AdditionalDocumentDetails = AdditionalDocumentDetails() private val dateUtil = DateUtil() private val imageUtil = ImageUtil() override fun onPreExecute() { progressBar.visibility = View.VISIBLE } override fun doInBackground(vararg: Void?): Exception? { try { val cardService: CardService = CardService.getInstance(isoDep) cardService.open() val service = PassportService( cardService, PassportService.NORMAL_MAX_TRANCEIVE_LENGTH, PassportService.DEFAULT_MAX_BLOCKSIZE, true, false ) service.open() var paceSucceeded = false try { val cardSecurityFile = CardSecurityFile(service.getInputStream(PassportService.EF_CARD_SECURITY)) val securityInfoCollection: Collection<SecurityInfo> = cardSecurityFile.securityInfos for (securityInfo: SecurityInfo? in securityInfoCollection) { if (securityInfo is PACEInfo) { val paceInfo: PACEInfo = securityInfo service.doPACE( bacKey, paceInfo.objectIdentifier, PACEInfo.toParameterSpec(paceInfo.parameterId), null ) paceSucceeded = true } } } catch (e: Exception) { Log.d("goodgood", "goodgood444") Log.w("TAG", e) } service.sendSelectApplet(paceSucceeded) if (!paceSucceeded) { try { service.getInputStream(PassportService.EF_COM).read() } catch (e: java.lang.Exception) { service.doBAC(bacKey) } } // -- Personal Details -- // val dg1In: CardFileInputStream = service.getInputStream(PassportService.EF_DG1) val dg1File = DG1File(dg1In) val mrzInfo: MRZInfo = dg1File.mrzInfo personDetails.documentType = mrzInfo.documentType.toString() personDetails.name = mrzInfo.secondaryIdentifier.replace("<", " ").trim { it <= ' ' } personDetails.surname = mrzInfo.primaryIdentifier.replace("<", " ").trim { it <= ' ' } personDetails.personalNumber = mrzInfo.personalNumber personDetails.gender = mrzInfo.gender.toString() personDetails.birthDate = dateUtil.convertFromMrzDate(mrzInfo.dateOfBirth) personDetails.expiryDate = dateUtil.convertFromMrzDate(mrzInfo.dateOfExpiry) personDetails.serialNumber = mrzInfo.documentNumber personDetails.nationality = mrzInfo.nationality personDetails.issuerAuthority = mrzInfo.issuingState Log.d("goodgood", personDetails.name!!) // -- Face Image -- // val dg2In: CardFileInputStream = service.getInputStream(PassportService.EF_DG2) val dg2File = DG2File(dg2In) val faceInfos: List<FaceInfo> = dg2File.faceInfos val allFaceImageInfos: MutableList<FaceImageInfo> = ArrayList() for (faceInfo: FaceInfo in faceInfos) { allFaceImageInfos.addAll(faceInfo.faceImageInfos) } if (allFaceImageInfos.isNotEmpty()) { val faceImageInfo: FaceImageInfo = allFaceImageInfos.iterator().next() val image: Image = imageUtil.getImage(mContext, faceImageInfo) personDetails.faceImage = image.bitmapImage personDetails.faceImageBase64 = image.base64Image } // -- Fingerprint (if exist)-- // try { val dg3In: CardFileInputStream = service.getInputStream(PassportService.EF_DG3) val dg3File = DG3File(dg3In) val fingerInfos = dg3File.fingerInfos val allFingerImageInfos: MutableList<FingerImageInfo> = ArrayList() for (fingerInfo in fingerInfos) { allFingerImageInfos.addAll(fingerInfo.fingerImageInfos) } val fingerprintsImage: MutableList<Bitmap> = ArrayList() if (allFingerImageInfos.isNotEmpty()) { for (fingerImageInfo in allFingerImageInfos) { val image: Image = imageUtil.getImage(mContext, fingerImageInfo) fingerprintsImage.add(image.bitmapImage) } personDetails.fingerprints = fingerprintsImage } } catch (e: java.lang.Exception) { Log.w("TAG", e) } // -- Portrait Picture -- // try { val dg5In: CardFileInputStream = service.getInputStream(PassportService.EF_DG5) val dg5File = DG5File(dg5In) val displayedImageInfos = dg5File.images if (displayedImageInfos.isNotEmpty()) { val displayedImageInfo: DisplayedImageInfo = displayedImageInfos.iterator().next() val image = imageUtil.getImage(mContext, displayedImageInfo) personDetails.portraitImage = image.bitmapImage personDetails.portraitImageBase64 = image.base64Image } } catch (e: java.lang.Exception) { Log.w("TAG", e) } // // -- Signature (if exist) -- // try { val dg7In: CardFileInputStream = service.getInputStream(PassportService.EF_DG7) val dg7File = DG7File(dg7In) val signatureImageInfos = dg7File.images if (signatureImageInfos.isNotEmpty()) { val displayedImageInfo: DisplayedImageInfo = signatureImageInfos.iterator().next() val image = imageUtil.getImage(mContext, displayedImageInfo) personDetails.portraitImage = image.bitmapImage personDetails.portraitImageBase64 = image.base64Image } } catch (e: java.lang.Exception) { Log.w("TAG", e) } // -- Additional Details (if exist) -- // try { val dg11In = service.getInputStream(PassportService.EF_DG11) val dg11File = DG11File(dg11In) if (dg11File.length > 0) { additionalPersonDetails.nameOfHolder = dg11File.nameOfHolder additionalPersonDetails.otherNames = dg11File.otherNames additionalPersonDetails.personalNumber = dg11File.personalNumber additionalPersonDetails.placeOfBirth = dg11File.placeOfBirth additionalPersonDetails.permanentAddress = dg11File.permanentAddress additionalPersonDetails.telephone = dg11File.telephone additionalPersonDetails.profession = dg11File.profession additionalPersonDetails.title = dg11File.title additionalPersonDetails.personalSummary = dg11File.personalSummary additionalPersonDetails.proofOfCitizenship = dg11File.proofOfCitizenship additionalPersonDetails.otherValidTDNumbers = dg11File.otherValidTDNumbers additionalPersonDetails.custodyInformation = dg11File.custodyInformation } } catch (e: java.lang.Exception) { Log.w("TAG", e) } try { val dg12In = service.getInputStream(PassportService.EF_DG12) val dg12File = DG12File(dg12In) additionalDocumentDetails.issuingAuthority = dg12File.issuingAuthority additionalDocumentDetails.dateOfIssue = dg12File.dateOfIssue additionalDocumentDetails.namesOfOtherPersons = dg12File.namesOfOtherPersons additionalDocumentDetails.endorsementsAndObservations = dg12File.endorsementsAndObservations additionalDocumentDetails.taxOrExitRequirements = dg12File.taxOrExitRequirements additionalDocumentDetails.imageOfFront = dg12File.imageOfFront additionalDocumentDetails.imageOfRear = dg12File.imageOfRear } catch (e: java.lang.Exception) { } eDocument.setAdditionalDocumentDetails(additionalDocumentDetails) eDocument.setPersonDetails(personDetails) eDocument.setAdditionalPersonDetails(additionalPersonDetails) } catch (e: java.lang.Exception) { return e } return null } override fun onPostExecute(exception: Exception?) { progressBar.visibility = View.INVISIBLE if (exception == null) { val intent = Intent(mContext, ResultActivity::class.java) intent.putExtra("EDOCUMENT", eDocument) mContext.startActivity(intent) } else { Log.d("onPostExecute", exception.toString()) } } } abstract class ThreadTask<T1, T2, T3> : Runnable { // Argument private var mArgument: T2? = null // Result private var mResult: T3? = null // Handle the result private val WORK_DONE = 0 // Execute fun execute() { onPreExecute() // Begin thread work val thread = Thread(this) thread.start() } override fun run() { // Call doInBackground mResult = doInBackground(mArgument) // Notify main thread that the work is done mResultHandler.sendEmptyMessage(0) } private var mResultHandler: Handler = object : Handler(Looper.getMainLooper()) { override fun handleMessage(msg: Message) { super.handleMessage(msg) // Call onPostExecute onPostExecute(mResult) } } // onPreExecute protected abstract fun onPreExecute() // doInBackground protected abstract fun doInBackground(vararg: T2?): T3? // onPostExecute protected abstract fun onPostExecute(result: T3?) }
AsyncTask로 NFC한 데이터를 불러오는 코드도 혹시 몰라 남겨둔다.
더보기class ReadTask constructor( private val isoDep: IsoDep, private val bacKey: BACKeySpec, val mContext: Context, ) : AsyncTask<Void?, Void?, Exception?>() { var eDocument: EDocument = EDocument() var personDetails: PersonDetails = PersonDetails() var additionalPersonDetails: AdditionalPersonDetails = AdditionalPersonDetails() val dateUtil = DateUtil() val imageUtil = ImageUtil() override fun doInBackground(vararg params: Void?): Exception? { try { val cardService: CardService = CardService.getInstance(isoDep) cardService.open() val service = PassportService( cardService, PassportService.NORMAL_MAX_TRANCEIVE_LENGTH, PassportService.DEFAULT_MAX_BLOCKSIZE, true, false ) service.open() var paceSucceeded = false try { val cardSecurityFile = CardSecurityFile(service.getInputStream(PassportService.EF_CARD_SECURITY)) val securityInfoCollection: Collection<SecurityInfo> = cardSecurityFile.securityInfos for (securityInfo: SecurityInfo? in securityInfoCollection) { if (securityInfo is PACEInfo) { val paceInfo: PACEInfo = securityInfo service.doPACE( bacKey, paceInfo.objectIdentifier, PACEInfo.toParameterSpec(paceInfo.parameterId), null ) paceSucceeded = true } } } catch (e: Exception) { Log.w("TAG", e) } service.sendSelectApplet(paceSucceeded) if (!paceSucceeded) { try { service.getInputStream(PassportService.EF_COM).read() } catch (e: java.lang.Exception) { service.doBAC(bacKey) } } // -- Personal Details -- // val dg1In: CardFileInputStream = service.getInputStream(PassportService.EF_DG1) val dg1File: DG1File = DG1File(dg1In) val mrzInfo: MRZInfo = dg1File.mrzInfo personDetails.name = mrzInfo.secondaryIdentifier.replace("<", " ").trim { it <= ' ' } personDetails.surname = mrzInfo.primaryIdentifier.replace("<", " ").trim { it <= ' ' } personDetails.personalNumber = mrzInfo.personalNumber personDetails.gender = mrzInfo.gender.toString() personDetails.birthDate = dateUtil.convertFromMrzDate(mrzInfo.dateOfBirth) personDetails.expiryDate = dateUtil.convertFromMrzDate(mrzInfo.dateOfExpiry) personDetails.serialNumber = mrzInfo.documentNumber personDetails.nationality = mrzInfo.nationality personDetails.issuerAuthority = mrzInfo.issuingState Log.d( "MRZInfoMRZInfo", "name : " + mrzInfo.secondaryIdentifier.replace("<", " ").trim { it <= ' ' }) // -- Face Image -- // val dg2In: CardFileInputStream = service.getInputStream(PassportService.EF_DG2) val dg2File = DG2File(dg2In) val faceInfos: List<FaceInfo> = dg2File.faceInfos val allFaceImageInfos: MutableList<FaceImageInfo> = ArrayList() for (faceInfo: FaceInfo in faceInfos) { allFaceImageInfos.addAll(faceInfo.faceImageInfos) } if (allFaceImageInfos.isNotEmpty()) { val faceImageInfo: FaceImageInfo = allFaceImageInfos.iterator().next() val image: Image = imageUtil.getImage(mContext, faceImageInfo) personDetails.faceImage = image.bitmapImage personDetails.faceImageBase64 = image.base64Image } // -- Fingerprint (if exist)-- // try { val dg3In: CardFileInputStream = service.getInputStream(PassportService.EF_DG3) val dg3File = DG3File(dg3In) val fingerInfos = dg3File.fingerInfos val allFingerImageInfos: MutableList<FingerImageInfo> = ArrayList() for (fingerInfo in fingerInfos) { allFingerImageInfos.addAll(fingerInfo.fingerImageInfos) } val fingerprintsImage: MutableList<Bitmap> = ArrayList() if (allFingerImageInfos.isNotEmpty()) { for (fingerImageInfo in allFingerImageInfos) { val image: Image = imageUtil.getImage(mContext, fingerImageInfo) fingerprintsImage.add(image.bitmapImage) } personDetails.fingerprints = fingerprintsImage } } catch (e: java.lang.Exception) { Log.w("TAG", e) } // -- Portrait Picture -- // try { val dg5In: CardFileInputStream = service.getInputStream(PassportService.EF_DG5) val dg5File = DG5File(dg5In) val displayedImageInfos = dg5File.images if (displayedImageInfos.isNotEmpty()) { val displayedImageInfo: DisplayedImageInfo = displayedImageInfos.iterator().next() val image = imageUtil.getImage(mContext, displayedImageInfo) personDetails.portraitImage = image.bitmapImage personDetails.portraitImageBase64 = image.base64Image } } catch (e: java.lang.Exception) { Log.w("TAG", e) } // // -- Signature (if exist) -- // try { val dg7In: CardFileInputStream = service.getInputStream(PassportService.EF_DG7) val dg7File = DG7File(dg7In) val signatureImageInfos = dg7File.images if (signatureImageInfos.isNotEmpty()) { val displayedImageInfo: DisplayedImageInfo = signatureImageInfos.iterator().next() val image = imageUtil.getImage(mContext, displayedImageInfo) personDetails.portraitImage = image.bitmapImage personDetails.portraitImageBase64 = image.base64Image } } catch (e: java.lang.Exception) { Log.w("TAG", e) } // -- Additional Details (if exist) -- // try { val dg11In = service.getInputStream(PassportService.EF_DG11) val dg11File = DG11File(dg11In) if (dg11File.length > 0) { additionalPersonDetails.custodyInformation = dg11File.custodyInformation additionalPersonDetails.nameOfHolder = dg11File.nameOfHolder additionalPersonDetails.fullDateOfBirth = dg11File.fullDateOfBirth additionalPersonDetails.otherNames = dg11File.otherNames additionalPersonDetails.otherValidTDNumbers = dg11File.otherValidTDNumbers additionalPersonDetails.permanentAddress = dg11File.permanentAddress additionalPersonDetails.personalNumber = dg11File.personalNumber additionalPersonDetails.personalSummary = dg11File.personalSummary additionalPersonDetails.placeOfBirth = dg11File.placeOfBirth additionalPersonDetails.profession = dg11File.profession additionalPersonDetails.proofOfCitizenship = dg11File.proofOfCitizenship additionalPersonDetails.tag = dg11File.tag additionalPersonDetails.tagPresenceList = dg11File.tagPresenceList additionalPersonDetails.telephone = dg11File.telephone additionalPersonDetails.title = dg11File.title } } catch (e: java.lang.Exception) { Log.w("TAG", e) } eDocument.setPersonDetails(personDetails) eDocument.setAdditionalPersonDetails(additionalPersonDetails) } catch (e: java.lang.Exception) { return e } return null } override fun onPostExecute(exception: Exception?) { Log.d("onPostExecute", "onPostExecute555") if (exception == null) { val intent = Intent(mContext, ResultActivity::class.java) intent.putExtra("EDOCUMENT", eDocument) mContext.startActivity(intent) } else { Log.d("onPostExecute", exception.toString()) } } }
아래는 NFC로 읽어올수 있는 데이터의 정보이다
DG1부터 DG16까지 여러가지 정보를 불러올수 있다. 다만 NFC칩에 들어있는 정보중 어떤건 필수로 들어있고 어떤건 선택적으로 들어있어서 신여권 구여권 각 나라별로 들어있는 정보가 다르다.
github 주소 :
https://github.com/tvroom88/AIO_Android_Kotlin_Support_Material/tree/main/CameraX/CameraX_5_OCR_NFC
'회사 생활 > 여권 NFC (CameraX + OCR + NFC)' 카테고리의 다른 글
CameraX api 5-1편 : OCR + 여권(MRZ + NFC) (0) 2023.11.24 CameraX api 번외편 : guideline에 따라 이미지 자르기 (1) 2023.11.20 CameraX api 4-2편 : Analysis + OCR + TextGraphic (1) 2023.11.18 CameraX api 4편 : Anlysis와 text 분석 라이브러리(OCR) (0) 2023.11.14 CameraX api 3편 : takePicture 메소드 (0) 2023.11.13