diff --git a/README.md b/README.md index 2bf59fb..ea619e1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# Acuant Android SDK v11.5.4 -**September 2022** +# Acuant Android SDK v11.6.0 +**February 2023** See [https://github.com/Acuant/AndroidSDKV11/releases](https://github.com/Acuant/AndroidSDKV11/releases) for release notes. @@ -22,7 +22,7 @@ This document provides detailed information about the Acuant Android SDK. The Ac ## Updates -**v11.5.0:** Please review [Migration Details](docs/MigrationDetails.md) for migration details (last updated for v11.5.0). +**v11.6.0:** Please review [Migration Details](docs/MigrationDetails.md) for migration details (last updated for v11.6.0). ---------- @@ -116,15 +116,15 @@ The SDK includes the following modules: - Add the following dependencies - implementation 'com.acuant:acuantcommon:11.5.4' - implementation 'com.acuant:acuantcamera:11.5.4' - implementation 'com.acuant:acuantimagepreparation:11.5.4' - implementation 'com.acuant:acuantdocumentprocessing:11.5.4' - implementation 'com.acuant:acuantechipreader:11.5.4' - implementation 'com.acuant:acuantipliveness:11.5.4' - implementation 'com.acuant:acuantfacematch:11.5.4' - implementation 'com.acuant:acuantfacecapture:11.5.4' - implementation 'com.acuant:acuantpassiveliveness:11.5.4' + implementation 'com.acuant:acuantcommon:11.6.0' + implementation 'com.acuant:acuantcamera:11.6.0' + implementation 'com.acuant:acuantimagepreparation:11.6.0' + implementation 'com.acuant:acuantdocumentprocessing:11.6.0' + implementation 'com.acuant:acuantechipreader:11.6.0' + implementation 'com.acuant:acuantipliveness:11.6.0' + implementation 'com.acuant:acuantfacematch:11.6.0' + implementation 'com.acuant:acuantfacecapture:11.6.0' + implementation 'com.acuant:acuantpassiveliveness:11.6.0' 1. Create an .xml file with the following tags. (If you plan to use bearer tokens to initialize, include only the endpoints.): @@ -191,7 +191,7 @@ Before you use the SDK, you must initialize it, either by using the credentials AcuantInitializer.initializeWithToken("PATH/TO/CONFIG/FILENAME.XML", token, context, - listOf(ImageProcessorInitializer(), EchipInitializer(), MrzCameraInitializer()), + listOf(MrzCameraInitializer()), listener) } catch(e: AcuantException) { Log.e("Acuant Error", e.toString()) @@ -202,7 +202,7 @@ Before you use the SDK, you must initialize it, either by using the credentials try { AcuantInitializer.initialize("PATH/TO/CONFIG/FILENAME.XML", context, - listOf(ImageProcessorInitializer(), EchipInitializer(), MrzCameraInitializer()), + listOf(MrzCameraInitializer()), listener) } catch(e: AcuantException) { Log.e("Acuant Error", e.toString()) @@ -211,19 +211,22 @@ Before you use the SDK, you must initialize it, either by using the credentials * Using credentials hardcoded in the code (not recommended): try { - Credential.init("xxxxxxx", - "xxxxxxxx", - "xxxxxxxxxx", - "https://frm.acuant.net", - "https://services.assureid.net", - "https://medicscan.acuant.net", - "https://us.passlive.acuant.net", - "https://acas.acuant.net", - "https://ozone.acuant.net") + Credential.init( + username: String, + password: String, + subscription: String?, + acasEndpoint: String, + assureIdEndpoint: String? = null, + frmEndpoint: String? = null, + passiveLivenessEndpoint: String? = null, + ipLivenessEndpoint: String? = null, + ozoneEndpoint: String? = null, + healthInsuranceEndpoint: String? = null + ) AcuantInitializer.initialize(null, context, - listOf(ImageProcessorInitializer(), EchipInitializer(), MrzCameraInitializer()), + listOf(MrzCameraInitializer()), listener) } catch(e: AcuantException) { Log.e("Acuant Error", e.toString()) @@ -273,7 +276,7 @@ The SDK can be initialized by providing only a username and a password. However, if (result.resultCode == RESULT_OK) { val data: Intent? = result.data - val url = data?.getStringExtra(ACUANT_EXTRA_IMAGE_URL) + val bytes = AcuantCameraActivity.getLatestCapturedBytes(clearBytesAfterRead = true) val barcodeString = data?.getStringExtra(ACUANT_EXTRA_PDF417_BARCODE) //... } else if (result.resultCode == RESULT_CANCELED) { @@ -284,7 +287,9 @@ The SDK can be initialized by providing only a username and a password. However, if (error is AcuantError) { //... } - } + } + +1. (Optional) Add localized strings in app's string resources as indicated [here](#language-localization) ### Capturing a document barcode ### @@ -321,6 +326,7 @@ The SDK can be initialized by providing only a username and a password. However, //... } } +1. (Optional) Add localized strings in app's string resources as indicated [here](#language-localization) ### Capturing MRZ data in a passport document ### @@ -334,7 +340,7 @@ The SDK can be initialized by providing only a username and a password. However, Capturing the MRZ data using AcuantCamera is similar to document capture. - 1. Start camera activity: + 1. Start camera activity: val cameraIntent = Intent( this@MainActivity, @@ -350,8 +356,7 @@ The SDK can be initialized by providing only a username and a password. However, //start activity for result - 1. Get activity result: - + 1. Get activity result: if (result.resultCode == RESULT_OK) { val data: Intent? = result.data @@ -365,8 +370,10 @@ The SDK can be initialized by providing only a username and a password. However, if (error is AcuantError) { //... } - } - + } + + 1. (Optional) Add localized strings in app's string resources as indicated [here](#language-localization) + ---------- ## AcuantImagePreparation ## @@ -387,7 +394,7 @@ This section describes how to use **AcuantImagePreparation**. passing in the cropping data: - class CroppingData(imageUrlString: String) + class CroppingData(imageBytes: ByteArray) and a callback listener: @@ -397,9 +404,9 @@ This section describes how to use **AcuantImagePreparation**. * **Important note:** Most listeners/callbacks in the SDK are extended off of AcuantListener, which contains the onError function shown below. - interface AcuantListener { - fun onError(error: AcuantError) - } + interface AcuantListener { + fun onError(error: AcuantError) + } The **AcuantImage** can be used to verify the crop, sharpness, and glare of the image, and then upload the document in the next step (see [AcuantDocumentProcessing](#acuantdocumentprocessing)). @@ -435,7 +442,7 @@ After you capture a document image and completed crop, it can be processed using class IdInstanceOptions ( val authenticationSensitivity: AuthenticationSensitivity, - val tamperSensitivity: TamperSensitivity, + val tamperSensitivity: AuthenticationSensitivity, val countryCode: String? ) @@ -533,7 +540,7 @@ For most workflows, the steps resemble the following, with reuploads on error or } }) -3. Get the facial capture result (call after onSuccess in IPLivenessListener): +1. Get the facial capture result (call after onSuccess in IPLivenessListener): AcuantIPLiveness.getFacialLiveness( token, @@ -549,7 +556,9 @@ For most workflows, the steps resemble the following, with reuploads on error or } ) -------------------------------------- +1. (Optional) Add localized strings in app's string resources as indicated [here](#language-localization) + +---------- ## AcuantFaceCapture ## @@ -581,11 +590,13 @@ This module is used to automate capturing an image of a face appropriate for use //error... } +3. (Optional) Add localized strings in app's string resources as indicated [here](#language-localization) + **Note:** HGLiveness/Blink Test Liveness can be accessed by modifying the options as follows: FaceCaptureOptions(cameraMode = CameraMode.HgLiveness) -------------------------------------- +---------- ## AcuantPassiveLiveness ## @@ -602,11 +613,17 @@ This module is used to determine liveness from a single selfie image. } class PassiveLivenessResult { - var livenessAssessment: LivenessAssessment? = null - var transactionId: String? = null - var score = 0 - var errorDesc: String? = null - var errorCode: PassiveLivenessErrorCode? = null + var livenessResult: LivenessResult? + var transactionId: String? + var errorDesc: String? + var unparsedErrorCode: String? + val errorCode: PassiveLivenessErrorCode? + } + + class LivenessResult { + var unparsedLivenessAssessment: String? + val livenessAssessment: LivenessAssessment? + var score: Int } enum class LivenessAssessment { @@ -695,6 +712,105 @@ Must include EchipInitializer() in initialization (See **Initializing the SDK**) ------------------------------------- +## Language localization + +In order to display texts in the corresponding language you need to add the following strings to your app's strings resources: + +#### AcuantCamera + + Info + This sample needs camera permission. + This device doesn\'t support Camera2 API. + + ALIGN + ALIGN AND TAP + MOVE CLOSER + TOO CLOSE + NOT IN FRAME + HOLD STEADY + CAPTURING + + Reading MRZ + Align + Move Closer + Try Repositioning + Read Successful + + Capturing... + Capture Barcode + +#### AcuantFaceCapture + + Align face to start capture + Too close! Move away + Move closer + Face has angle. Do not tilt + Move in frame + Hold steady + Please blink + Capturing… + + Capturing\n%d… + Capturing\n%d… + + +#### AcuantIPLiveness + + en + Put your face in the oval + Fill the oval with your face + Put your face in the frame + Connecting… + Tap the screen to begin + Move closer + Go somewhere shadier + Streaming… + Streaming, network is slow… + Scanning… + Identifying face… + Confirming identity… + Assessing genuine presence… + Assessing liveness… + Loading… + Creating identity… + Finding face… + Authenticate + Enrol + %1$s to %2$s + Too close + Ambiguous outcome + Please do not move while iProoving + Ambient light too strong or screen brightness too low + Strong light source detected behind you + Your environment appears too dark + Too much light detected on your face + Please do not talk while iProoving + Your session has expired + %1$s as %2$s to %3$s + Avoid tilting your head + Avoid tilting your head + Turn slightly to your right + Turn slightly to your left + Hold the device at eye level + Hold the device at eye level + Scan completed + Get ready… + Loading… + Network error + Device is not supported + Camera error + Camera permission denied + Please allow camera access for this app in Android Settings + Server error + Application is in multi-window mode + Face detector error + An existing capture is already in progress + Before calling IProov.launch(), you should register a listener with IProov.registerListener() + Invalid iProov options + Unexpected error + +---------- + ### Error codes ### object ErrorCodes { @@ -896,13 +1012,39 @@ Must include EchipInitializer() in initialization (See **Initializing the SDK**) ## Frequently Asked Questions ## +#### Why is the SDK so large #### + +The SDK is large because there are several ml-kit models bundled into it. This bundling has pros and cons. Bundling models into the SDK enables it to work in areas and on devices that do not have access to Google Play services. If the size of the SDK is an issue, you can reduce the size by downloading the models from Google Play services the first time the application is launched. The download can occur in the background. To enable the download, use the open versions of the face capture and camera modules. Within the Gradles of those models, remove the following lines: + + implementation 'com.google.mlkit:face-detection:XXX' + implementation 'com.google.mlkit:barcode-scanning:XXX' + +and replace them with the following respectively (where XXX is the current version number on the previous lines): + + implementation 'com.google.android.gms:play-services-mlkit-face-detection:XXX' + implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:XXX' + +Then, add the following to your manifest: + + + ... + + + +**Important:** If the models can’t be accessed, the SDK behaves unexpectedly. Use this size-saving procedure only if you are sure that all of your clients will use phones that have access to Google Play from regions that don’t block it. + +To read more about this and the ml-kit, see https://developers.google.com/ml-kit/guides. + + #### How do I obfuscate my Android application? #### Acuant does not provide obfuscation tools. See the Android developer documentation about obfuscation at: [https://developer.android.com/studio/build/shrink-code](https://developer.android.com/studio/build/shrink-code). Then open proguard-rules.pro for your project and set the obfuscation rules. ------------------------------------- -**Copyright 2022 Acuant Inc. All rights reserved.** +**Copyright 2023 Acuant Inc. All rights reserved.** This document contains proprietary and confidential information and creative works owned by Acuant and its respective licensors, if any. Any use, copying, publication, distribution, display, modification, or transmission of such technology, in whole or in part, in any form or by any means, without the prior express written permission of Acuant is strictly prohibited. Except where expressly provided by Acuant in writing, possession of this information shall not be construed to confer any license or rights under any Acuant intellectual property rights, whether by estoppel, implication, or otherwise. diff --git a/acuantcamera/build.gradle b/acuantcamera/build.gradle index 019dafa..bcc4d05 100644 --- a/acuantcamera/build.gradle +++ b/acuantcamera/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'kotlin-android' apply plugin: 'maven-publish' android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 - targetSdkVersion 32 + targetSdkVersion 33 } buildTypes { @@ -33,11 +33,11 @@ android { } dependencies { // Kotlin lang - implementation 'androidx.core:core-ktx:1.8.0' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1' // App compat and UI things - implementation 'androidx.appcompat:appcompat:1.4.2' + implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' @@ -49,9 +49,9 @@ dependencies { implementation "androidx.camera:camera-view:$camerax_version" //acuant specific stuff - implementation 'androidx.exifinterface:exifinterface:1.3.3' + implementation 'androidx.exifinterface:exifinterface:1.3.5' implementation 'com.google.mlkit:barcode-scanning:17.0.2' implementation 'com.rmtheis:tess-two:9.1.0' - implementation 'com.acuant:acuantcommon:11.5.4' - implementation 'com.acuant:acuantimagepreparation:11.5.4' + implementation 'com.acuant:acuantcommon:11.6.0' + implementation 'com.acuant:acuantimagepreparation:11.6.0' } diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantBaseCameraFragment.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantBaseCameraFragment.kt index c029a14..82fea31 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantBaseCameraFragment.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantBaseCameraFragment.kt @@ -354,14 +354,20 @@ abstract class AcuantBaseCameraFragment: Fragment() { val exif = Exif.createFromFileString(savedUri) val rotation = exif.rotation + val file = File(savedUri) + if (captureType != null) { - addExif(File(savedUri), captureType, rotation) + addExif(file, captureType, rotation) } else { - addExif(File(savedUri), "NOT SPECIFIED (implementer used deprecated constructor that lacks this data)", rotation) + addExif(file, "NOT SPECIFIED (implementer used deprecated constructor that lacks this data)", rotation) } fullyDone = true - listener.onSaved(savedUri) + + val bytes = file.readBytes() + file.delete() + + listener.onSaved(bytes) } override fun onError(exception: ImageCaptureException) { diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraActivity.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraActivity.kt index 98372a2..7971d1b 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraActivity.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraActivity.kt @@ -1,6 +1,7 @@ package com.acuant.acuantcamera.camera import android.content.Intent +import android.os.Build import android.os.Bundle import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity @@ -24,14 +25,17 @@ class AcuantCameraActivity: AppCompatActivity(), ICameraActivityFinish { super.onCreate(savedInstanceState) binding = ActivityCameraBinding.inflate(layoutInflater) - val unserializedOptions = intent.getSerializableExtra(ACUANT_EXTRA_CAMERA_OPTIONS) - val options: AcuantCameraOptions = if (unserializedOptions == null) { - AcuantCameraOptions.DocumentCameraOptionsBuilder().build() + var options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent?.getSerializableExtra(ACUANT_EXTRA_CAMERA_OPTIONS, AcuantCameraOptions::class.java) } else { - unserializedOptions as AcuantCameraOptions + @Suppress("DEPRECATION") + intent?.getSerializableExtra(ACUANT_EXTRA_CAMERA_OPTIONS) as AcuantCameraOptions? } + if (options == null) + options = AcuantCameraOptions.DocumentCameraOptionsBuilder().build() + if (options.preventScreenshots) { window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) } @@ -59,10 +63,11 @@ class AcuantCameraActivity: AppCompatActivity(), ICameraActivityFinish { } } - //Camera Responses - override fun onCameraDone(imageUrl: String, barCodeString: String?) { + override fun onCameraDone(imageBytes: ByteArray, barCodeString: String?) { val intent = Intent() - intent.putExtra(ACUANT_EXTRA_IMAGE_URL, imageUrl) + //We can not do this as the maximum transaction size is way too small. +// intent.putExtra(ACUANT_EXTRA_IMAGE_BYTES, imageBytes) + AcuantCameraActivity.imageBytes = imageBytes intent.putExtra(ACUANT_EXTRA_PDF417_BARCODE, barCodeString) this@AcuantCameraActivity.setResult(RESULT_OK, intent) this@AcuantCameraActivity.finish() @@ -109,4 +114,23 @@ class AcuantCameraActivity: AppCompatActivity(), ICameraActivityFinish { super.onResume() hideTopMenu() } + + companion object { + private var imageBytes: ByteArray? = null + + @JvmStatic + @Synchronized + fun getLatestCapturedBytes(clearBytesAfterRead: Boolean): ByteArray? { + val bytes = imageBytes?.clone() + if (clearBytesAfterRead) { + if (imageBytes != null) { + for (i in imageBytes!!.indices) { + imageBytes!![i] = (0).toByte() + } + imageBytes = null + } + } + return bytes + } + } } \ No newline at end of file diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraOptions.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraOptions.kt index 7bd3b87..0a46f36 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraOptions.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraOptions.kt @@ -139,6 +139,7 @@ internal constructor( } @Deprecated("No longer reliant on GMS, option is ignored", ReplaceWith("")) + @Suppress("unused") fun setUseGms(value: Boolean) : DocumentCameraOptionsBuilder { return this } diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/document/AcuantDocCameraFragment.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/document/AcuantDocCameraFragment.kt index 9ab63b4..ed241f6 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/document/AcuantDocCameraFragment.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/document/AcuantDocCameraFragment.kt @@ -27,7 +27,7 @@ import kotlin.math.pow import kotlin.math.roundToInt import kotlin.math.sqrt -enum class DocumentCameraState { Align, MoveCloser, MoveBack, HoldSteady, CountingDown, Capturing } +enum class DocumentCameraState { Align, NotInFrame, MoveCloser, MoveBack, HoldSteady, CountingDown, Capturing } class AcuantDocCameraFragment: AcuantBaseCameraFragment() { @@ -78,7 +78,7 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { if (!firstThreeTimings.contains((-1).toLong())) { hasFinishedTest = true - if (firstThreeTimings.minOrNull() ?: (TOO_SLOW_FOR_AUTO_THRESHOLD + 10) > TOO_SLOW_FOR_AUTO_THRESHOLD) { + if ((firstThreeTimings.minOrNull() ?: (TOO_SLOW_FOR_AUTO_THRESHOLD + 10)) > TOO_SLOW_FOR_AUTO_THRESHOLD) { setTapToCapture() } } @@ -96,8 +96,8 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { detectedPoints = PointsUtils.fixPoints(PointsUtils.scalePoints(detectedPoints, camContainer, analyzerSize, previewSize, rectangleView)) realDpi = PointsUtils.scaleDpi(analyzerDPI, analyzerSize, imageCapture?.resolutionInfo?.resolution) if (previewSize != null) { - val mult = 0.02f - val view = Rect((previewSize.left * (1 + mult)).toInt(), (previewSize.top * (1 + mult)).toInt(), (previewSize.right * (1 - mult)).toInt(), (previewSize.bottom * (1 - mult)).toInt()) + val insetFromEdges = 0.02f + val view = Rect((previewSize.left * (1 + insetFromEdges)).toInt(), (previewSize.top * (1 + insetFromEdges)).toInt(), (previewSize.right * (1 - insetFromEdges)).toInt(), (previewSize.bottom * (1 - insetFromEdges)).toInt()) var isContained = true detectedPoints.forEach { if (!view.contains(it.y, it.x)) { @@ -107,7 +107,7 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { if (isContained) { docState } else { - DocumentState.NoDocument + DocumentState.OutOfBounds } } else { docState @@ -122,6 +122,11 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { setTextFromState(DocumentCameraState.Align) resetWorkflow() } + DocumentState.OutOfBounds -> { + rectangleView?.setViewFromState(DocumentCameraState.NotInFrame) + setTextFromState(DocumentCameraState.NotInFrame) + resetWorkflow() + } DocumentState.TooClose -> { rectangleView?.setViewFromState(DocumentCameraState.MoveBack) setTextFromState(DocumentCameraState.MoveBack) @@ -160,8 +165,8 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { else -> { val middle = PointsUtils.findMiddleForCamera(points, fragmentCameraBinding?.root?.width, fragmentCameraBinding?.root?.height) captureImage(object : IAcuantSavedImage { - override fun onSaved(uri: String) { - cameraActivityListener.onCameraDone(uri, latestBarcode) + override fun onSaved(bytes: ByteArray) { + cameraActivityListener.onCameraDone(bytes, latestBarcode) } override fun onError(error: AcuantError) { @@ -246,7 +251,16 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { context?.resources?.getDimension(R.dimen.cam_info_width)?.toInt() ?: 300 textView.textSize = context?.resources?.getDimension(R.dimen.cam_doc_font) ?: 24f - textView.text = getString(R.string.acuant_camera_not_in_frame) + textView.text = getString(R.string.acuant_camera_too_close) + textView.setTextColor(Color.WHITE) + } + DocumentCameraState.NotInFrame -> { + textView.background = defaultTextDrawable + textView.layoutParams?.width = + context?.resources?.getDimension(R.dimen.cam_info_width)?.toInt() ?: 300 + textView.textSize = + context?.resources?.getDimension(R.dimen.cam_doc_font) ?: 24f + textView.text = getString(R.string.acuant_camera_out_of_bounds) textView.setTextColor(Color.WHITE) } DocumentCameraState.CountingDown -> { @@ -350,8 +364,8 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { textView?.setBackgroundColor(greenTransparent) textView?.text = getString(R.string.acuant_camera_capturing) captureImage(object : IAcuantSavedImage { - override fun onSaved(uri: String) { - cameraActivityListener.onCameraDone(uri, latestBarcode) + override fun onSaved(bytes: ByteArray) { + cameraActivityListener.onCameraDone(bytes, latestBarcode) } override fun onError(error: AcuantError) { diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/constant/Constants.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/constant/Constants.kt index c3934f1..8346c1b 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/constant/Constants.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/constant/Constants.kt @@ -2,6 +2,8 @@ package com.acuant.acuantcamera.constant +@Suppress("unused") +@Deprecated("This form of returning an image has been removed", ReplaceWith("AcuantCameraActivity.getLatestCapturedBytes()")) const val ACUANT_EXTRA_IMAGE_URL = "imageUrl" const val ACUANT_EXTRA_PDF417_BARCODE = "barCodeString" const val ACUANT_EXTRA_CAMERA_OPTIONS = "cameraOptions" diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/DocumentFrameResult.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/DocumentFrameResult.kt index b6d8b93..1c19dcc 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/DocumentFrameResult.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/DocumentFrameResult.kt @@ -4,7 +4,7 @@ import android.graphics.Point enum class DocumentType {Id, Passport, Other} -enum class DocumentState { NoDocument, TooFar, TooClose, GoodDocument } +enum class DocumentState { NoDocument, TooFar, TooClose, GoodDocument, OutOfBounds } class DocumentFrameResult (val points: Array?, val currentDistRatio: Float?, diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/initializer/MrzCameraInitializer.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/initializer/MrzCameraInitializer.kt index e66f4f9..8f436c1 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/initializer/MrzCameraInitializer.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/initializer/MrzCameraInitializer.kt @@ -15,20 +15,16 @@ import java.lang.Exception class MrzCameraInitializer : IAcuantPackage { override fun initialize(credential: Credential, context: Context, callback: IAcuantPackageCallback) { - if(credential.secureAuthorizations != null) { - try { - initializeOcr(context) - } catch (e: Exception) { - e.printStackTrace() - callback.onInitializeFailed( - listOf(AcuantError(ErrorCodes.ERROR_FailedToLoadOcrFiles, - ErrorDescriptions.ERROR_DESC_FailedToLoadOcrFiles, e.toString()))) - return - } - callback.onInitializeSuccess() - } else { - callback.onInitializeFailed(listOf(AcuantError(ErrorCodes.ERROR_InvalidCredentials, ErrorDescriptions.ERROR_DESC_InvalidCredentials))) + try { + initializeOcr(context) + } catch (e: Exception) { + e.printStackTrace() + callback.onInitializeFailed( + listOf(AcuantError(ErrorCodes.ERROR_FailedToLoadOcrFiles, + ErrorDescriptions.ERROR_DESC_FailedToLoadOcrFiles, e.toString()))) + return } + callback.onInitializeSuccess() } @Throws(IOException::class) diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/interfaces/IAcuantSavedImage.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/interfaces/IAcuantSavedImage.kt index 7b997d4..57d99e7 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/interfaces/IAcuantSavedImage.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/interfaces/IAcuantSavedImage.kt @@ -3,5 +3,5 @@ package com.acuant.acuantcamera.interfaces import com.acuant.acuantcommon.background.AcuantListener interface IAcuantSavedImage: AcuantListener { - fun onSaved(uri: String) + fun onSaved(bytes: ByteArray) } \ No newline at end of file diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/interfaces/ICameraActivityFinish.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/interfaces/ICameraActivityFinish.kt index 566e27e..2eaa96f 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/interfaces/ICameraActivityFinish.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/interfaces/ICameraActivityFinish.kt @@ -3,8 +3,8 @@ package com.acuant.acuantcamera.interfaces import com.acuant.acuantcamera.helper.MrzResult import com.acuant.acuantcommon.background.AcuantListener -interface ICameraActivityFinish : AcuantListener{ - fun onCameraDone(imageUrl: String, barCodeString: String?) +interface ICameraActivityFinish : AcuantListener { + fun onCameraDone(imageBytes: ByteArray, barCodeString: String?) fun onCameraDone(mrzResult: MrzResult) fun onCameraDone(barCodeString: String) fun onCancel() diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/overlay/BaseRectangleView.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/overlay/BaseRectangleView.kt index 4236939..1ddb067 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/overlay/BaseRectangleView.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/overlay/BaseRectangleView.kt @@ -104,8 +104,8 @@ abstract class BaseRectangleView(context: Context, attr: AttributeSet?) : View(c textureViewWidth = width } - fun setAndDrawPoints(points:Array?){ - if(this.points != null) { + fun setAndDrawPoints(points:Array?) { + if (this.points != null) { oldPoints = this.points } this.points = points @@ -128,7 +128,7 @@ abstract class BaseRectangleView(context: Context, attr: AttributeSet?) : View(c private fun calcPath() { path = Path() - if(points != null && points!!.size == 4) { + if (points != null && points!!.size == 4) { path.moveTo(points!![3].y.toFloat(), points!![3].x.toFloat()) path.lineTo(points!![0].y.toFloat(), points!![0].x.toFloat()) @@ -139,10 +139,11 @@ abstract class BaseRectangleView(context: Context, attr: AttributeSet?) : View(c } internal fun setDrawBox(drawBox : Boolean) { - if(allowBox) + if (allowBox) { this.drawBox = drawBox - else + } else { this.drawBox = false + } } override fun onDraw(canvas: Canvas) { @@ -181,7 +182,7 @@ abstract class BaseRectangleView(context: Context, attr: AttributeSet?) : View(c frame!!.top.toFloat(), frame!!.right.toFloat(), frame!!.top.toFloat(), frame!!.left.toFloat()) } else { - if(oldPoints == null || oldPoints!!.size != 4) { + if (oldPoints == null || oldPoints!!.size != 4) { drawBracketsFromCords(canvas, points!![0].x.toFloat(), points!![0].y.toFloat(), points!![1].x.toFloat(), points!![1].y.toFloat(), points!![2].x.toFloat(), points!![2].y.toFloat(), points!![3].x.toFloat(), points!![3].y.toFloat()) } else { diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/overlay/DocRectangleView.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/overlay/DocRectangleView.kt index 8331e72..0c9cccb 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/overlay/DocRectangleView.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/overlay/DocRectangleView.kt @@ -7,25 +7,16 @@ import com.acuant.acuantcamera.camera.document.DocumentCameraState class DocRectangleView(context: Context, attr: AttributeSet?) : BaseRectangleView(context, attr) { fun setViewFromState(state: DocumentCameraState) { when(state) { - DocumentCameraState.MoveCloser -> { + DocumentCameraState.MoveCloser, + DocumentCameraState.MoveBack, + DocumentCameraState.NotInFrame -> { setDrawBox(false) paint.color = paintColorCloser paintBracket.color = paintColorBracketCloser animateTarget = false } - DocumentCameraState.MoveBack -> { - setDrawBox(false) - paint.color = paintColorCloser - paintBracket.color = paintColorBracketCloser - animateTarget = false - } - DocumentCameraState.CountingDown -> { - setDrawBox(true) - paint.color = paintColorHold - paintBracket.color = paintColorBracketHold - animateTarget = true - } - DocumentCameraState.HoldSteady -> { + DocumentCameraState.CountingDown, + DocumentCameraState.HoldSteady-> { setDrawBox(true) paint.color = paintColorHold paintBracket.color = paintColorBracketHold diff --git a/acuantcamera/src/main/res/values/strings.xml b/acuantcamera/src/main/res/values/strings.xml index 98114b4..2b0430b 100644 --- a/acuantcamera/src/main/res/values/strings.xml +++ b/acuantcamera/src/main/res/values/strings.xml @@ -25,7 +25,8 @@ ALIGN ALIGN AND TAP MOVE CLOSER - TOO CLOSE! + TOO CLOSE + NOT IN FRAME HOLD STEADY CAPTURING @@ -35,10 +36,10 @@ Try Repositioning Read Successful Camera Load Error - ok + OK This application cannot run because it does not have the camera permission. The application will now exit. The camera is required to capture documents. If the permission has been declined you will need to manually go to the app settings to enable it. An error occurred while starting the camera, possibly in use by another application. - Capturing... + Capturing… Capture Barcode diff --git a/acuantfacecapture/build.gradle b/acuantfacecapture/build.gradle index 10969e1..2cf78f2 100644 --- a/acuantfacecapture/build.gradle +++ b/acuantfacecapture/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'kotlin-android' apply plugin: 'maven-publish' android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 - targetSdkVersion 32 + targetSdkVersion 33 testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } @@ -29,11 +29,11 @@ android { } dependencies { // Kotlin lang - implementation 'androidx.core:core-ktx:1.8.0' + implementation 'androidx.core:core-ktx:1.9.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1' // App compat and UI things - implementation 'androidx.appcompat:appcompat:1.4.2' + implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' @@ -46,8 +46,8 @@ dependencies { //acuant specific stuff implementation 'com.google.mlkit:face-detection:16.1.5' - implementation 'com.acuant:acuantcommon:11.5.4' - implementation 'com.acuant:acuantimagepreparation:11.5.4' + implementation 'com.acuant:acuantcommon:11.6.0' + implementation 'com.acuant:acuantimagepreparation:11.6.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' diff --git a/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/camera/AcuantFaceCameraActivity.kt b/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/camera/AcuantFaceCameraActivity.kt index 89798ba..db06769 100644 --- a/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/camera/AcuantFaceCameraActivity.kt +++ b/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/camera/AcuantFaceCameraActivity.kt @@ -1,6 +1,7 @@ package com.acuant.acuantfacecapture.camera import android.content.Intent +import android.os.Build import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat @@ -30,14 +31,16 @@ class AcuantFaceCameraActivity: AppCompatActivity(), IFaceCameraActivityFinish { setContentView(binding.root) hideTopMenu() - val unserializedOptions = intent.getSerializableExtra(ACUANT_EXTRA_FACE_CAPTURE_OPTIONS) - - val options: FaceCaptureOptions = if (unserializedOptions == null) { - FaceCaptureOptions() + var options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent?.getSerializableExtra(ACUANT_EXTRA_FACE_CAPTURE_OPTIONS, FaceCaptureOptions::class.java) } else { - unserializedOptions as FaceCaptureOptions + @Suppress("DEPRECATION") + intent?.getSerializableExtra(ACUANT_EXTRA_FACE_CAPTURE_OPTIONS) as FaceCaptureOptions? } + if (options == null) + options = FaceCaptureOptions() + //start the camera if this is the first time the activity is created (camera already exists otherwise) if (savedInstanceState == null) { val cameraFragment: AcuantBaseFaceCameraFragment = when (options.cameraMode) { diff --git a/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/camera/facecapture/AcuantFaceCaptureFragment.kt b/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/camera/facecapture/AcuantFaceCaptureFragment.kt index 7b52440..128e53e 100644 --- a/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/camera/facecapture/AcuantFaceCaptureFragment.kt +++ b/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/camera/facecapture/AcuantFaceCaptureFragment.kt @@ -36,6 +36,7 @@ class AcuantFaceCaptureFragment: AcuantBaseFaceCameraFragment() { private var userHasBlinked = false private var userHasHadOpenEyes = false private var lastState: FaceState = FaceState.NoFace + private var isMovingCloser = false override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -95,7 +96,7 @@ class AcuantFaceCaptureFragment: AcuantBaseFaceCameraFragment() { startTime = System.currentTimeMillis() } - private fun onFaceDetected(rect: Rect?, state: FaceState) { + private fun onFaceDetected(rect: Rect?, state: FaceState, sizeRatio: Float) { if (capturing || !isAdded) return val analyzerSize = imageAnalyzer?.resolutionInfo?.resolution @@ -126,6 +127,7 @@ class AcuantFaceCaptureFragment: AcuantBaseFaceCameraFragment() { } FaceState.FaceTooFar -> { mFacialGraphicOverlay?.setState(FaceCameraState.MoveCloser) + isMovingCloser = true mFacialGraphic?.updateLiveFaceDetails(boundingBox, FaceCameraState.MoveCloser) resetTimer() } @@ -140,40 +142,47 @@ class AcuantFaceCaptureFragment: AcuantBaseFaceCameraFragment() { resetTimer() } else -> { //good face or closed eyes - when { - didFaceMove(boundingBox, lastFacePosition) -> { - mFacialGraphicOverlay?.setState(FaceCameraState.KeepSteady) - mFacialGraphic?.updateLiveFaceDetails(boundingBox, FaceCameraState.KeepSteady) - resetTimer() - } - requireBlink && !userHasBlinked -> { - if (userHasHadOpenEyes && lastState == FaceState.EyesClosed && realState == FaceState.GoodFace) { - userHasBlinked = true + if (isMovingCloser && sizeRatio < FaceFrameAnalyzer.TOO_FAR_THRESH_WITH_BUFFER) { + mFacialGraphicOverlay?.setState(FaceCameraState.MoveCloser) + mFacialGraphic?.updateLiveFaceDetails(boundingBox, FaceCameraState.MoveCloser) + resetTimer() + } else { + isMovingCloser = false + when { + didFaceMove(boundingBox, lastFacePosition) -> { + mFacialGraphicOverlay?.setState(FaceCameraState.KeepSteady) + mFacialGraphic?.updateLiveFaceDetails(boundingBox, FaceCameraState.KeepSteady) + resetTimer() } - if (realState == FaceState.GoodFace) { - userHasHadOpenEyes = true + requireBlink && !userHasBlinked -> { + if (userHasHadOpenEyes && lastState == FaceState.EyesClosed && realState == FaceState.GoodFace) { + userHasBlinked = true + } + if (realState == FaceState.GoodFace) { + userHasHadOpenEyes = true + } + mFacialGraphicOverlay?.setState(FaceCameraState.Blink) + mFacialGraphic?.updateLiveFaceDetails(boundingBox, FaceCameraState.Blink) + resetTimer(resetBlinkState = false) } - mFacialGraphicOverlay?.setState(FaceCameraState.Blink) - mFacialGraphic?.updateLiveFaceDetails(boundingBox, FaceCameraState.Blink) - resetTimer(resetBlinkState = false) - } - System.currentTimeMillis() - startTime < acuantOptions.totalCaptureTime * 1000 -> { - mFacialGraphicOverlay?.setState(FaceCameraState.Hold, acuantOptions.totalCaptureTime - ceil(((System.currentTimeMillis() - startTime) / 1000).toDouble()).toInt()) - mFacialGraphic?.updateLiveFaceDetails(boundingBox, FaceCameraState.Hold) - } - else -> { - mFacialGraphicOverlay?.setState(FaceCameraState.Capturing) - mFacialGraphic?.updateLiveFaceDetails(boundingBox, FaceCameraState.Capturing) - if (!capturing) { - captureImage(object : IAcuantSavedImage { - override fun onSaved(uri: String) { - cameraActivityListener.onCameraDone(uri) - } - - override fun onError(error: AcuantError) { - cameraActivityListener.onError(error) - } - }) + System.currentTimeMillis() - startTime < acuantOptions.totalCaptureTime * 1000 -> { + mFacialGraphicOverlay?.setState(FaceCameraState.Hold, acuantOptions.totalCaptureTime - ceil(((System.currentTimeMillis() - startTime) / 1000).toDouble()).toInt()) + mFacialGraphic?.updateLiveFaceDetails(boundingBox, FaceCameraState.Hold) + } + else -> { + mFacialGraphicOverlay?.setState(FaceCameraState.Capturing) + mFacialGraphic?.updateLiveFaceDetails(boundingBox, FaceCameraState.Capturing) + if (!capturing) { + captureImage(object : IAcuantSavedImage { + override fun onSaved(uri: String) { + cameraActivityListener.onCameraDone(uri) + } + + override fun onError(error: AcuantError) { + cameraActivityListener.onError(error) + } + }) + } } } } @@ -185,8 +194,8 @@ class AcuantFaceCaptureFragment: AcuantBaseFaceCameraFragment() { override fun buildImageAnalyzer(screenAspectRatio: Int, rotation: Int) { val frameAnalyzer = try { - FaceFrameAnalyzer { boundingBox, state -> - onFaceDetected(boundingBox, state) + FaceFrameAnalyzer { boundingBox, state, sizeRatio -> + onFaceDetected(boundingBox, state, sizeRatio) } } catch (e: IllegalStateException) { cameraActivityListener.onError(AcuantError(ErrorCodes.ERROR_UnexpectedError, ErrorDescriptions.ERROR_DESC_UnexpectedError, e.toString())) @@ -203,7 +212,7 @@ class AcuantFaceCaptureFragment: AcuantBaseFaceCameraFragment() { } companion object { - private const val MOVEMENT_THRESHOLD = 22 + private const val MOVEMENT_THRESHOLD = 32 private fun didFaceMove(facePosition: Rect?, lastFacePosition: Rect?): Boolean { if (facePosition == null || lastFacePosition == null) diff --git a/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/detector/FaceFrameAnalyzer.kt b/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/detector/FaceFrameAnalyzer.kt index 4287466..81aebc7 100644 --- a/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/detector/FaceFrameAnalyzer.kt +++ b/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/detector/FaceFrameAnalyzer.kt @@ -11,7 +11,7 @@ import kotlin.math.abs import kotlin.math.max import kotlin.math.min -typealias FaceFrameListener = (boundingBox: Rect?, state: FaceState) -> Unit +typealias FaceFrameListener = (boundingBox: Rect?, state: FaceState, sizeRatio: Float) -> Unit enum class FaceState { NoFace, FaceTooFar, FaceTooClose, FaceAngled, EyesClosed, GoodFace @@ -32,6 +32,7 @@ class FaceFrameAnalyzer internal constructor(private val listener: FaceFrameList } var faceState = FaceState.NoFace var detectedBounds: Rect? = null + var sizeRatio = 0f running = true val mediaImage = image.image //don't close this one @@ -49,7 +50,7 @@ class FaceFrameAnalyzer internal constructor(private val listener: FaceFrameList // like the shape of a face bounds = Rect((bounds.centerX() - bounds.width() * 0.5f * 0.85f).toInt(), bounds.top, (bounds.centerX() + bounds.width() * 0.5f * 0.85f).toInt(), bounds.bottom) detectedBounds = bounds - val sizeRatio = sizeRatio(bounds, origSize) + sizeRatio = sizeRatio(bounds, origSize) val rotY = face.headEulerAngleY val rotZ = face.headEulerAngleZ @@ -63,7 +64,8 @@ class FaceFrameAnalyzer internal constructor(private val listener: FaceFrameList abs(rotY) > Y_ROT_ANGLE || abs(rotZ) > Z_ROT_ANGLE -> { faceState = FaceState.FaceAngled } - face.rightEyeOpenProbability ?: 1f < EYE_CLOSED_THRESHOLD && face.leftEyeOpenProbability ?: 1f < EYE_CLOSED_THRESHOLD -> { + (face.rightEyeOpenProbability ?: 1f) < EYE_CLOSED_THRESHOLD && + (face.leftEyeOpenProbability ?: 1f) < EYE_CLOSED_THRESHOLD -> { faceState = FaceState.EyesClosed } else -> { @@ -76,13 +78,13 @@ class FaceFrameAnalyzer internal constructor(private val listener: FaceFrameList // .addOnFailureListener { e -> //catch // } .addOnCompleteListener { //finally - listener(detectedBounds, faceState) + listener(detectedBounds, faceState, sizeRatio) running = false image.close() } } else { running = false - listener(detectedBounds, faceState) + listener(detectedBounds, faceState, sizeRatio) image.close() } } @@ -94,8 +96,9 @@ class FaceFrameAnalyzer internal constructor(private val listener: FaceFrameList .setMinFaceSize(0.5f) .build()) - private const val TOO_CLOSE_THRESH = 0.815f - private const val TOO_FAR_THRESH = 0.635f + const val TOO_CLOSE_THRESH = 0.785f + const val TOO_FAR_THRESH = 0.5125f + const val TOO_FAR_THRESH_WITH_BUFFER = TOO_FAR_THRESH + 0.05f private const val EYE_CLOSED_THRESHOLD = 0.3f private const val Y_ROT_ANGLE = 10 private const val Z_ROT_ANGLE = 15 diff --git a/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/overlays/FacialGraphic.kt b/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/overlays/FacialGraphic.kt index af86892..f060300 100644 --- a/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/overlays/FacialGraphic.kt +++ b/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/overlays/FacialGraphic.kt @@ -1,6 +1,8 @@ package com.acuant.acuantfacecapture.overlays +import android.animation.ValueAnimator import android.graphics.* +import android.view.animation.LinearInterpolator import com.acuant.acuantfacecapture.camera.facecapture.FaceCameraState import com.acuant.acuantfacecapture.model.FaceCaptureOptions @@ -15,6 +17,9 @@ internal class FacialGraphic(overlay: FacialGraphicOverlay) : FacialGraphicOverl private var state = FaceCameraState.Align private val mFaceRectPaint: Paint = Paint() private var width = 0 + private var bracketAnimator : ValueAnimator? = null + private var distanceMoved : Float = 0f + private var oldPoints: Rect? = null init { mFaceRectPaint.color = options?.colorGood ?: Color.GREEN @@ -30,6 +35,19 @@ internal class FacialGraphic(overlay: FacialGraphicOverlay) : FacialGraphicOverl fun updateLiveFaceDetails(faceBounds: Rect?, state: FaceCameraState) { this.faceBounds = faceBounds this.state = state + if (this.faceBounds != null) { + oldPoints = this.faceBounds + } + bracketAnimator?.cancel() + bracketAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + addUpdateListener { + distanceMoved = it.animatedValue as Float + postInvalidate() + } + duration = 250L + interpolator = LinearInterpolator() + start() + } postInvalidate() } @@ -49,19 +67,57 @@ internal class FacialGraphic(overlay: FacialGraphicOverlay) : FacialGraphicOverl else -> mFaceRectPaint.color = options?.colorGood ?: Color.GREEN } - if (options != null && options!!.showOval) { - drawFaceOval(canvas) - } else { - drawFaceBrackets(canvas) + val options = options + if (options != null) { + drawFace(canvas, options.showOval) } canvas.restore() } - private fun drawFaceBrackets(canvas: Canvas) { + private fun drawFace(canvas: Canvas, oval: Boolean) { val position = faceBounds + val oldPoints = oldPoints if (position != null) { - drawBracketsFromCords(canvas, position.bottom.toFloat(), position.left.toFloat(), position.bottom.toFloat(), position.right.toFloat(), position.top.toFloat(), position.right.toFloat(), position.top.toFloat(), position.left.toFloat()) + if (oldPoints == null) { + if (oval) { + drawFaceOval(canvas, position.left.toFloat(), + position.top.toFloat(), position.right.toFloat(), + position.bottom.toFloat()) + } else { + drawBracketsFromCords( + canvas, + position.bottom.toFloat(), + position.left.toFloat(), + position.bottom.toFloat(), + position.right.toFloat(), + position.top.toFloat(), + position.right.toFloat(), + position.top.toFloat(), + position.left.toFloat() + ) + } + } else { + if (oval) { + val bottom = oldPoints.bottom + (position.bottom - oldPoints.bottom) * distanceMoved + val left = oldPoints.left + (position.left - oldPoints.left) * distanceMoved + val right = oldPoints.right + (position.right - oldPoints.right) * distanceMoved + val top = oldPoints.top + (position.top - oldPoints.top) * distanceMoved + + drawFaceOval(canvas, left, top, right, bottom) + } else { + val x0 = oldPoints.bottom + (position.bottom - oldPoints.bottom) * distanceMoved + val y0 = oldPoints.left + (position.left - oldPoints.left) * distanceMoved + val x1 = oldPoints.bottom + (position.bottom - oldPoints.bottom) * distanceMoved + val y1 = oldPoints.right + (position.right - oldPoints.right) * distanceMoved + val x2 = oldPoints.top + (position.top - oldPoints.top) * distanceMoved + val y2 = oldPoints.right + (position.right - oldPoints.right) * distanceMoved + val x3 = oldPoints.top + (position.top - oldPoints.top) * distanceMoved + val y3 = oldPoints.left + (position.left - oldPoints.left) * distanceMoved + + drawBracketsFromCords(canvas, x0, y0, x1, y1, x2, y2, x3, y3) + } + } } } @@ -85,12 +141,10 @@ internal class FacialGraphic(overlay: FacialGraphicOverlay) : FacialGraphicOverl } - private fun drawFaceOval(canvas: Canvas) { + private fun drawFaceOval(canvas: Canvas, left: Float, top: Float, right: Float, bottom: Float) { val position = faceBounds if (position != null) { - canvas.drawOval(position.left.toFloat(), - position.top.toFloat(), position.right.toFloat(), - position.bottom.toFloat(), mFaceRectPaint) + canvas.drawOval(left, top, right, bottom, mFaceRectPaint) } } diff --git a/app/build.gradle b/app/build.gradle index 39d8404..f3d920e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,13 +5,13 @@ repositories { maven { url 'https://raw.githubusercontent.com/iProov/android/master/maven/' } } dependencies { - implementation "androidx.core:core-ktx:1.8.0" - implementation 'androidx.appcompat:appcompat:1.4.2' + implementation "androidx.core:core-ktx:1.9.0" + implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.exifinterface:exifinterface:1.3.3' + implementation 'androidx.exifinterface:exifinterface:1.3.5' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'com.google.android.material:material:1.6.1' - implementation "androidx.activity:activity-ktx:1.5.1" + implementation 'com.google.android.material:material:1.7.0' + implementation "androidx.activity:activity-ktx:1.6.1" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' @@ -19,26 +19,26 @@ dependencies { //If you do not plan to modify the camera code you can use these two regular maven dependencies // in place of the project ones. -// implementation 'com.acuant:acuantfacecapture:11.5.4' -// implementation 'com.acuant:acuantcamera:11.5.4' +// implementation 'com.acuant:acuantfacecapture:11.6.0' +// implementation 'com.acuant:acuantcamera:11.6.0' implementation project(path: ':acuantcamera') implementation project(path: ':acuantfacecapture') - implementation 'com.acuant:acuantcommon:11.5.4' - implementation 'com.acuant:acuantimagepreparation:11.5.4' - implementation 'com.acuant:acuantdocumentprocessing:11.5.4' - implementation 'com.acuant:acuantechipreader:11.5.4' - implementation 'com.acuant:acuantfacematch:11.5.4' - implementation 'com.acuant:acuantipliveness:11.5.4' - implementation 'com.acuant:acuantpassiveliveness:11.5.4' + implementation 'com.acuant:acuantcommon:11.6.0' + implementation 'com.acuant:acuantimagepreparation:11.6.0' + implementation 'com.acuant:acuantdocumentprocessing:11.6.0' + implementation 'com.acuant:acuantechipreader:11.6.0' + implementation 'com.acuant:acuantfacematch:11.6.0' + implementation 'com.acuant:acuantipliveness:11.6.0' + implementation 'com.acuant:acuantpassiveliveness:11.6.0' } android { - compileSdkVersion 32 - buildToolsVersion '32' + compileSdkVersion 33 + buildToolsVersion '33' defaultConfig { applicationId "com.acuant.sampleapp" minSdkVersion 21 - targetSdkVersion 32 + targetSdkVersion 33 versionCode 1 versionName multiDexEnabled true diff --git a/app/src/main/java/com/acuant/sampleapp/MainActivity.kt b/app/src/main/java/com/acuant/sampleapp/MainActivity.kt index 1f1d7ab..a7b58a5 100644 --- a/app/src/main/java/com/acuant/sampleapp/MainActivity.kt +++ b/app/src/main/java/com/acuant/sampleapp/MainActivity.kt @@ -32,9 +32,12 @@ import com.acuant.acuantcommon.model.* import com.acuant.acuantcommon.helper.CredentialHelper import com.acuant.acuantdocumentprocessing.AcuantDocumentProcessor import com.acuant.acuantdocumentprocessing.model.* -import com.acuant.acuantdocumentprocessing.resultmodel.* +import com.acuant.acuantdocumentprocessing.documentresultmodel.* +import com.acuant.acuantdocumentprocessing.documentresultmodel.enums.DocumentDataType +import com.acuant.acuantdocumentprocessing.documentresultmodel.enums.DocumentSide +import com.acuant.acuantdocumentprocessing.documentresultmodel.enums.LightSource +import com.acuant.acuantdocumentprocessing.healthinsuranceresultmodel.HealthInsuranceCardResult import com.acuant.acuantdocumentprocessing.service.listener.* -import com.acuant.acuantechipreader.initializer.EchipInitializer import com.acuant.acuantfacecapture.camera.AcuantFaceCameraActivity import com.acuant.acuantfacecapture.constant.Constants.ACUANT_EXTRA_FACE_CAPTURE_OPTIONS import com.acuant.acuantfacecapture.constant.Constants.ACUANT_EXTRA_FACE_IMAGE_URL @@ -46,7 +49,6 @@ import com.acuant.acuantfacematchsdk.model.FacialMatchResult import com.acuant.acuantfacematchsdk.service.FacialMatchListener import com.acuant.acuantimagepreparation.AcuantImagePreparation import com.acuant.acuantimagepreparation.background.EvaluateImageListener -import com.acuant.acuantimagepreparation.initializer.ImageProcessorInitializer import com.acuant.acuantimagepreparation.model.AcuantImage import com.acuant.acuantimagepreparation.model.CroppingData import com.acuant.acuantipliveness.AcuantIPLiveness @@ -59,6 +61,8 @@ import com.acuant.acuantpassiveliveness.AcuantPassiveLiveness import com.acuant.acuantpassiveliveness.model.PassiveLivenessData import com.acuant.acuantpassiveliveness.model.PassiveLivenessResult import com.acuant.acuantpassiveliveness.service.PassiveLivenessListener +import com.acuant.sampleapp.NfcResultActivity.Companion.FACE_LIVENESS_RESULT +import com.acuant.sampleapp.NfcResultActivity.Companion.FACE_MATCH_RESULT import com.acuant.sampleapp.backgroundtasks.AcuantTokenService import com.acuant.sampleapp.backgroundtasks.AcuantTokenServiceListener import com.acuant.sampleapp.utils.CommonUtils @@ -66,8 +70,6 @@ import com.google.android.material.switchmaterial.SwitchMaterial import java.io.* import java.net.HttpURLConnection import java.net.URL -import java.util.* -import kotlin.collections.HashMap import kotlin.concurrent.thread class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { @@ -78,8 +80,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { private var isHealthCard: Boolean = false private var capturingImageData: Boolean = true private var capturingSelfieImage: Boolean = false - private var capturingFacialMatch: Boolean = false - private var facialResultString: String? = null + private var faceMatchResultString: String? = null private var facialLivelinessResultString: String? = null private var documentIdInstance: AcuantIdDocumentInstance? = null private var documentHealthInstance: AcuantHealthInsuranceDocumentInstance? = null @@ -89,6 +90,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { private var livenessSelected = 0 private var isKeyless = false private var processingFacialLiveness = false + private var processingFacialMatch: Boolean = false private var usingPassive = true private var recentImage: AcuantImage? = null private val useTokenInit = true @@ -126,7 +128,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { } fun cleanUpTransaction() { - facialResultString = null + faceMatchResultString = null capturedFrontImage = null capturedBackImage = null capturedSelfieImage = null @@ -193,15 +195,18 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { private fun initializeAcuantSdk(callback: IAcuantPackageCallback) { try { // Or, if required to initialize without a config file , then can be done the following way - /*Credential.init("**username**", - "**password**", - "**subscription**", - "https://frm.acuant.net", - "https://services.assureid.net", - "https://medicscan.acuant.net", - "https://us.passlive.acuant.net", - "https://acas.acuant.net", - "https://ozone.acuant.net") + /*Credential.init( + username: String, + password: String, + subscription: String?, + acasEndpoint: String, + assureIdEndpoint: String? = null, + frmEndpoint: String? = null, + passiveLivenessEndpoint: String? = null, + ipLivenessEndpoint: String? = null, + ozoneEndpoint: String? = null, + healthInsuranceEndpoint: String? = null + ) AcuantInitializer.initialize(null, ...proceed as normal from here... */ @@ -224,7 +229,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { override fun onInitializeSuccess() { isInitialized = true - if(Credential.get().subscription == null || Credential.get().subscription.isEmpty() ){ + if (Credential.get().subscription?.isEmpty() != false) { isKeyless = true livenessSpinner.visibility = View.INVISIBLE callback.onInitializeSuccess() @@ -259,7 +264,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { val task = AcuantInitializer.initializeWithToken("acuant.config.xml", token, this@MainActivity, - listOf(ImageProcessorInitializer(), EchipInitializer(), MrzCameraInitializer()), + listOf(MrzCameraInitializer()), initCallback) if (task != null) backgroundTasks.add(task) @@ -281,26 +286,31 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { if (useTokenInit) { Toast.makeText(this@MainActivity, "Using Token Init", Toast.LENGTH_SHORT).show() Credential.initFromXml("acuant.config.xml", this) - if (Credential.get().token != null && Credential.get().token.isValid) { - finishTokenInit(Credential.get().token.value) + val token = Credential.get().token + if (token != null && token.isValid) { + finishTokenInit(token.value) } else { Credential.removeToken() - val task = AcuantTokenService(object : AcuantTokenServiceListener { - override fun onSuccess(token: String) { - finishTokenInit(token) - } + if (Credential.get().endpoints.isAcasEndpointValid) { + val task = AcuantTokenService(object : AcuantTokenServiceListener { + override fun onSuccess(token: String) { + finishTokenInit(token) + } - override fun onError(error: AcuantError) { - initCallback.onInitializeFailed(listOf(error)) - } - }).execute() - backgroundTasks.add(task) + override fun onError(error: AcuantError) { + initCallback.onInitializeFailed(listOf(error)) + } + }).execute() + backgroundTasks.add(task) + } else { + initCallback.onInitializeFailed(listOf(AcuantError(ErrorCodes.ERROR_InvalidEndpoint, ErrorDescriptions.ERROR_DESC_InvalidEndpoint))) + } } } else { Toast.makeText(this@MainActivity, "Using Credential Init", Toast.LENGTH_SHORT).show() val task = AcuantInitializer.initialize("acuant.config.xml", this@MainActivity, - listOf(ImageProcessorInitializer(), EchipInitializer(), MrzCameraInitializer()), + listOf(MrzCameraInitializer()), initCallback) if (task != null) backgroundTasks.add(task) @@ -466,7 +476,6 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { } override fun onError(error: AcuantError) { - capturingSelfieImage = false facialLivelinessResultString = "Facial Liveliness Failed" showAcuDialog(error, "Error Retrieving Facial Data") } @@ -499,7 +508,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { if (!hasInternetConnection()) { showAcuDialog("No internet connection available.") } else { - if (isInitialized && (!useTokenInit || Credential.get()?.token?.isValid == true)) { + if (isInitialized && (!useTokenInit || Credential.get().token?.isValid == true)) { cleanUpTransaction() showDocumentCaptureCamera() } else { @@ -526,7 +535,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { if (!hasInternetConnection()) { showAcuDialog("No internet connection available.") } else { - if (isInitialized && (!useTokenInit || Credential.get()?.token?.isValid == true)) { + if (isInitialized && (!useTokenInit || Credential.get().token?.isValid == true)) { cleanUpTransaction() isHealthCard = true showDocumentCaptureCamera() @@ -556,7 +565,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { if (!hasInternetConnection()) { showAcuDialog("No internet connection available.") } else { - if (isInitialized && (!useTokenInit || Credential.get()?.token?.isValid == true)) { + if (isInitialized && (!useTokenInit || Credential.get().token?.isValid == true)) { cleanUpTransaction() showMrzHelpScreen() } else { @@ -592,11 +601,11 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { private var docCameraLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { val data: Intent? = result.data - val url = data?.getStringExtra(ACUANT_EXTRA_IMAGE_URL) + val bytes = AcuantCameraActivity.getLatestCapturedBytes(clearBytesAfterRead = true) capturedBarcodeString = data?.getStringExtra(ACUANT_EXTRA_PDF417_BARCODE) - if (url != null) { + if (bytes != null) { setProgress(true, "Cropping...") - AcuantImagePreparation.evaluateImage(this, CroppingData(url), object : EvaluateImageListener { + AcuantImagePreparation.evaluateImage(this, CroppingData(bytes), object : EvaluateImageListener { override fun onSuccess(image: AcuantImage) { @@ -621,14 +630,19 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { } }) } else { - showAcuDialog("Camera failed to return valid image path") + showAcuDialog("Camera failed to return valid image bytes") } } else if (result.resultCode == RESULT_CANCELED) { Log.d(TAG, "User canceled document capture") } else { val data: Intent? = result.data - val error = data?.getSerializableExtra(ACUANT_EXTRA_ERROR) - if (error is AcuantError) { + val error = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + data?.getSerializableExtra(ACUANT_EXTRA_ERROR, AcuantError::class.java) + } else { + @Suppress("DEPRECATION") + data?.getSerializableExtra(ACUANT_EXTRA_ERROR) as AcuantError? + } + if (error != null) { showAcuDialog(error) } } @@ -655,16 +669,18 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { private var mrzCameraLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { val data: Intent? = result.data - val mrzResult = data?.getSerializableExtra(ACUANT_EXTRA_MRZ_RESULT) as MrzResult? - - val confirmNFCDataActivity = Intent(this, NfcConfirmationActivity::class.java) - confirmNFCDataActivity.putExtra("DOB", mrzResult?.dob) - confirmNFCDataActivity.putExtra("DOE", mrzResult?.passportExpiration) - confirmNFCDataActivity.putExtra("DOCNUMBER", mrzResult?.passportNumber) - confirmNFCDataActivity.putExtra("COUNTRY", mrzResult?.country) - confirmNFCDataActivity.putExtra("THREELINE", mrzResult?.threeLineMrz) + val mrzResult = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + data?.getSerializableExtra(ACUANT_EXTRA_MRZ_RESULT, MrzResult::class.java) + } else { + @Suppress("DEPRECATION") + data?.getSerializableExtra(ACUANT_EXTRA_MRZ_RESULT) as MrzResult? + } - this.startActivity(confirmNFCDataActivity) + if (mrzResult != null) { + showNfcConfirmation(mrzResult) + } else { + showAcuDialog("MRZ Read Error", "MRZ was returned blank, or missformatted.") + } } else if (result.resultCode == RESULT_CANCELED) { Log.d(TAG, "User canceled mrz capture") } @@ -683,6 +699,52 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { mrzCameraLauncher.launch(cameraIntent) } + private var nfcLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val nfcData = NfcStore.cardDetails + if (nfcData != null) { + + facialLivelinessResultString = null + faceMatchResultString = null + capturingImageData = false + + if (livenessSelected != 0) { + setProgress(true, "Getting Data...") + capturedDocumentFaceImage = nfcData.faceImage + showFaceCamera() + } + + thread { + while (capturingSelfieImage || processingFacialLiveness || processingFacialMatch) { + Thread.sleep(100) + } + + setProgress(false) + + val intent = Intent(this@MainActivity, NfcResultActivity::class.java) + intent.putExtra(FACE_LIVENESS_RESULT, facialLivelinessResultString) + intent.putExtra(FACE_MATCH_RESULT, faceMatchResultString) + startActivity(intent) + } + } else { + //this shouldn't really happen + showAcuDialog("NFC Data was null", "Activity returned OK code, but no Nfc Data.") + } + } //else cancelled, we don't care + } + + private fun showNfcConfirmation(mrzResult: MrzResult) { + + val confirmNFCDataActivity = Intent(this, NfcConfirmationActivity::class.java) + confirmNFCDataActivity.putExtra("DOB", mrzResult.dob) + confirmNFCDataActivity.putExtra("DOE", mrzResult.passportExpiration) + confirmNFCDataActivity.putExtra("DOCNUMBER", mrzResult.passportNumber) + confirmNFCDataActivity.putExtra("COUNTRY", mrzResult.country) + confirmNFCDataActivity.putExtra("THREELINE", mrzResult.threeLineMrz) + + nfcLauncher.launch(confirmNFCDataActivity) + } + private var barcodeCameraLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { val data: Intent? = result.data @@ -728,7 +790,6 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { alert.setNegativeButton("CANCEL") { dialog, _ -> setProgress(true, "Getting Data...") facialLivelinessResultString = "Facial Liveliness Failed" - capturingSelfieImage = false getData() dialog.dismiss() } @@ -901,6 +962,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { } private var faceCameraLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + capturingSelfieImage = false when (result.resultCode) { RESULT_OK -> { val data = result.data @@ -909,14 +971,12 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { if (url == null) { showFaceCaptureError() } else { - processingFacialLiveness = true - val bytes = readFromFile(url) capturedSelfieImage = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) val plData = PassiveLivenessData(capturedSelfieImage as Bitmap) AcuantPassiveLiveness.processFaceLiveness(plData, object : PassiveLivenessListener { override fun passiveLivenessFinished(result: PassiveLivenessResult) { - facialLivelinessResultString = when (result.livenessAssessment) { + facialLivelinessResultString = when (result.livenessResult?.livenessAssessment) { AcuantPassiveLiveness.LivenessAssessment.Live -> { "Facial Liveliness: live" } @@ -965,6 +1025,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { } private var hgCameraLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + capturingSelfieImage = false when (result.resultCode) { RESULT_OK -> { val data = result.data @@ -1003,6 +1064,8 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { when (livenessSelected) { 1 -> { capturingSelfieImage = true + processingFacialMatch = true + processingFacialLiveness = true if (usingPassive) { if (!isKeyless) { showFaceCapture() @@ -1015,6 +1078,8 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { } else -> { capturingSelfieImage = false + processingFacialMatch = false + processingFacialLiveness = false //just go to results } } @@ -1041,10 +1106,12 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { } override fun onSuccess(userId: String, token: String, frame: Bitmap?) { + capturingSelfieImage = false startFacialLivelinessRequest(token, userId) } override fun onFail(error: AcuantError) { + capturingSelfieImage = false showFaceCaptureError() } @@ -1055,6 +1122,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { } override fun onError(error: AcuantError) { + capturingSelfieImage = false showAcuDialog(error) } }) @@ -1193,45 +1261,56 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { setProgress(true, "Classifying...") val task = instance.getClassification(object : ClassificationListener { override fun documentClassified(classified: Boolean, classification: Classification) { - setProgress(false) - if (classified) { - frontCaptured = true - barcodeExpected = classification.type?.referenceDocumentDataTypes?.contains(0) ?: false - if (isBackSideRequired(classification)) { - this@MainActivity.runOnUiThread { - showAcuDialog(R.string.scan_back_side_id, "Message", { dialog, _ -> - dialog.dismiss() - showDocumentCaptureCamera() - }, { dialog, _ -> - dialog.dismiss() - }) - } - } else { - val alert = AlertDialog.Builder(this@MainActivity) - alert.setTitle("Message") - if (livenessSelected != 0) { - alert.setMessage("Capture Selfie Image") + runOnUiThread { + setProgress(false) + if (classified) { + frontCaptured = true + capturedFrontImage = null + barcodeExpected = + classification.type?.referenceDocumentDataTypes?.contains( + DocumentDataType.Barcode2D + ) ?: false + if (isBackSideRequired(classification)) { + this@MainActivity.runOnUiThread { + showAcuDialog( + R.string.scan_back_side_id, + "Message", + { dialog, _ -> + dialog.dismiss() + showDocumentCaptureCamera() + }, + { dialog, _ -> + dialog.dismiss() + }) + } } else { - alert.setMessage("Continue") - } - alert.setPositiveButton("OK") { dialog, _ -> - dialog.dismiss() - setProgress(true, "Getting Data...") - showFaceCamera() - getData() - } - if (livenessSelected != 0) { - alert.setNegativeButton("CANCEL") { dialog, _ -> - facialLivelinessResultString = "Facial Liveliness Failed" + val alert = AlertDialog.Builder(this@MainActivity) + alert.setTitle("Message") + if (livenessSelected != 0) { + alert.setMessage("Capture Selfie Image") + } else { + alert.setMessage("Continue") + } + alert.setPositiveButton("OK") { dialog, _ -> + dialog.dismiss() setProgress(true, "Getting Data...") + showFaceCamera() getData() - dialog.dismiss() } + if (livenessSelected != 0) { + alert.setNegativeButton("CANCEL") { dialog, _ -> + facialLivelinessResultString = + "Facial Liveliness Failed" + setProgress(true, "Getting Data...") + getData() + dialog.dismiss() + } + } + alert.show() } - alert.show() + } else { + showClassificationError() } - } else { - showClassificationError() } } @@ -1264,6 +1343,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { val task = instance.uploadBackImage(backData, object : UploadImageListener { override fun imageUploaded() { + capturedBackImage = null if (barcodeExpected && capturedBarcodeString != null) { val task = instance.uploadBarcode(BarcodeData(capturedBarcodeString!!), object : UploadBarcodeListener { override fun barcodeUploaded() { @@ -1294,7 +1374,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { if (instance != null) { val task = instance.getData(object : GetIdDataListener { override fun processingResultReceived(result: IDResult) { - if (result.fields == null || result.fields.dataFieldReferences == null) { + if (result.fields.isEmpty()) { showAcuDialog("Unknown error happened.\nCould not extract data") return } @@ -1305,7 +1385,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { var signImageUri: String? = null var faceImageUri: String? = null var resultString: String? = "" - val fieldReferences = result.fields.dataFieldReferences + val fieldReferences = result.fields for (reference in fieldReferences) { if (reference.key == "Document Class Name" && reference.type == "string") { if (reference.value == "Driver License") { @@ -1314,7 +1394,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { cardType = "ID3" } } else if (reference.key == "Document Number" && reference.type == "string") { - docNumber = reference.value + docNumber = reference.value ?: "" } else if (reference.key == "Photo" && reference.type == "uri") { faceImageUri = reference.value } else if (reference.key == "Signature" && reference.type == "uri") { @@ -1322,22 +1402,24 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { } } - for (image in result.images.images) { - if (image.side == 0) { + val images = result.images + for (image in images) { + if (image.side == DocumentSide.Front) { frontImageUri = image.uri - } else if (image.side == 1) { + } else if (image.side == DocumentSide.Back) { backImageUri = image.uri } } for (reference in fieldReferences) { if (reference.type == "string") { - resultString = resultString + reference.key + ":" + reference.value + System.lineSeparator() + resultString = + resultString + reference.key + ":" + reference.value + System.lineSeparator() } } resultString = "Authentication Result : " + - AuthenticationResult.getString(Integer.parseInt(result.result)) + + result.result + System.lineSeparator() + System.lineSeparator() + resultString @@ -1364,7 +1446,7 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { } }) - showResults(result.biographic.birthDate, result.biographic.expirationDate, docNumber, frontImage, backImage, faceImage, signImage, resultString, cardType) + showResults(result.biographic?.birthDate, result.biographic?.expirationDate, docNumber, frontImage, backImage, faceImage, signImage, resultString, cardType) } } } @@ -1381,7 +1463,6 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { //process Facial Match fun processFacialMatch() { - //MainActivity@ capturingFacialMatch = true thread { while (capturingImageData) { Thread.sleep(100) @@ -1394,26 +1475,23 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { if (facialMatchData.faceImageOne != null && facialMatchData.faceImageTwo != null) { val task = AcuantFaceMatch.processFacialMatch(facialMatchData, object : FacialMatchListener { override fun facialMatchFinished(result: FacialMatchResult) { - capturingSelfieImage = false - capturingFacialMatch = false this@MainActivity.runOnUiThread { - facialResultString = "isMatch: ${result.isMatch}\n" - facialResultString += "score: ${result.score}\n" - facialResultString += "transactionId: ${result.transactionId}\n" + faceMatchResultString = "isMatch: ${result.isMatch}\n" + faceMatchResultString += "score: ${result.score}\n" + faceMatchResultString += "transactionId: ${result.transactionId}\n" } + processingFacialMatch = false } override fun onError(error: AcuantError) { - capturingSelfieImage = false - capturingFacialMatch = false + processingFacialMatch = false showAcuDialog(error) } }) backgroundTasks.add(task) } else { - capturingSelfieImage = false - capturingFacialMatch = false + processingFacialMatch = false } } } @@ -1446,12 +1524,12 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { ProcessedData.documentNumber = documentNumber ProcessedData.cardType = cardType thread { - while (capturingFacialMatch || processingFacialLiveness) { + while (capturingSelfieImage || processingFacialLiveness || processingFacialMatch) { Thread.sleep(100) } this@MainActivity.runOnUiThread { - facialResultString = if (facialResultString == null) "Facial Match Failed" else facialResultString - ProcessedData.formattedString = (facialLivelinessResultString ?: "No Liveness Test Performed") + System.lineSeparator() + facialResultString+ System.lineSeparator() + resultString + faceMatchResultString = if (faceMatchResultString == null) "Facial Match Failed" else faceMatchResultString + ProcessedData.formattedString = (facialLivelinessResultString ?: "No Liveness Test Performed") + System.lineSeparator() + faceMatchResultString+ System.lineSeparator() + resultString val resultIntent = Intent( this@MainActivity, ResultActivity::class.java @@ -1478,13 +1556,14 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { fun isBackSideRequired(classification : Classification?): Boolean { var isBackSideScanRequired = false - if (classification?.type != null && classification.type.supportedImages != null) { - val list = classification.type.supportedImages as ArrayList> - for (i in list.indices) { - val map = list[i] - if (map["Light"] == 0) { - if (map["Side"] == 1) { - isBackSideScanRequired = true + if (classification?.type != null && classification.type?.supportedImages != null) { + val list = classification.type?.supportedImages + if (list != null) { + for (supportedImage in list) { + if (supportedImage.light == LightSource.White) { + if (supportedImage.side == DocumentSide.Back) { + isBackSideScanRequired = true + } } } } diff --git a/app/src/main/java/com/acuant/sampleapp/NfcConfirmationActivity.kt b/app/src/main/java/com/acuant/sampleapp/NfcConfirmationActivity.kt index f53baa7..6de9c50 100644 --- a/app/src/main/java/com/acuant/sampleapp/NfcConfirmationActivity.kt +++ b/app/src/main/java/com/acuant/sampleapp/NfcConfirmationActivity.kt @@ -209,14 +209,15 @@ class NfcConfirmationActivity : AppCompatActivity(), NfcTagReadingListener { } override fun tagReadSucceeded(nfcData: NfcData) { - setProgress(false) - error = false - val intent = Intent(this, NfcResultActivity::class.java) - NfcStore.cardDetails = nfcData - startActivity(intent) if (this.nfcAdapter != null) { this.nfcAdapter!!.disableForegroundDispatch(this) } + setProgress(false) + error = false + val intent = Intent() + NfcStore.cardDetails = nfcData + this.setResult(RESULT_OK, intent) + this.finish() } override fun tagReadStatus(status: String) { diff --git a/app/src/main/java/com/acuant/sampleapp/NfcResultActivity.kt b/app/src/main/java/com/acuant/sampleapp/NfcResultActivity.kt index 7ad03ee..d8d6a50 100644 --- a/app/src/main/java/com/acuant/sampleapp/NfcResultActivity.kt +++ b/app/src/main/java/com/acuant/sampleapp/NfcResultActivity.kt @@ -16,6 +16,11 @@ import com.acuant.acuantechipreader.model.NfcData class NfcResultActivity : AppCompatActivity() { + companion object { + const val FACE_MATCH_RESULT = "FaceMatchResult" + const val FACE_LIVENESS_RESULT = "FaceLivenessResult" + } + private var progressDialog: LinearLayout? = null private var progressText: TextView? = null private var resultLayout: RelativeLayout? = null @@ -56,11 +61,17 @@ class NfcResultActivity : AppCompatActivity() { val intent = intent if (intent != null) { - val cardDetails = NfcStore.cardDetails + val cardDetails: NfcData? = NfcStore.cardDetails + val faceMatch = intent.getStringExtra(FACE_MATCH_RESULT) + val faceLiveness = intent.getStringExtra(FACE_LIVENESS_RESULT) if (cardDetails != null) { setProgress(false) + if (faceLiveness != null && faceMatch != null) { + addField("Face Liveness", faceLiveness) + addField("Face Match", faceMatch) + } setData(cardDetails) - val image = cardDetails.image + val image = cardDetails.faceImage if (image != null) { imageView!!.setImageBitmap(image) } diff --git a/app/src/main/java/com/acuant/sampleapp/NfcStore.kt b/app/src/main/java/com/acuant/sampleapp/NfcStore.kt index 9c9d899..b2786cf 100644 --- a/app/src/main/java/com/acuant/sampleapp/NfcStore.kt +++ b/app/src/main/java/com/acuant/sampleapp/NfcStore.kt @@ -3,5 +3,7 @@ package com.acuant.sampleapp import com.acuant.acuantechipreader.model.NfcData object NfcStore { + //This is a bit sloppy, but NfcData is not fully serializable due to Bitmaps, so this is a + // simple solution. Either parcelable or implementing serialized bitmaps might be neater. var cardDetails: NfcData? = null } diff --git a/app/src/main/java/com/acuant/sampleapp/ResultActivity.kt b/app/src/main/java/com/acuant/sampleapp/ResultActivity.kt index 4ac0496..821ba3d 100644 --- a/app/src/main/java/com/acuant/sampleapp/ResultActivity.kt +++ b/app/src/main/java/com/acuant/sampleapp/ResultActivity.kt @@ -30,28 +30,28 @@ class ResultActivity : AppCompatActivity() { textViewCardInfo = findViewById(R.id.textViewLicenseCardInfo) nfcScanningBtn = findViewById(R.id.buttonNFC) - if(ProcessedData.cardType.equals("ID3",true) && (Credential.get().secureAuthorizations.ozoneAuth || Credential.get().secureAuthorizations.chipExtract)){ + if (ProcessedData.cardType.equals("ID3",true) && (Credential.get().secureAuthorizations.ozoneAuth || Credential.get().secureAuthorizations.chipExtract)) { nfcScanningBtn.visibility = View.VISIBLE - }else{ + } else { nfcScanningBtn.visibility = View.GONE } - if(ProcessedData.frontImage != null){ + if (ProcessedData.frontImage != null) { frontSideCardImageView.setImageBitmap(ProcessedData.frontImage) } - if(ProcessedData.backImage != null){ + if (ProcessedData.backImage != null) { backSideCardImageView.setImageBitmap(ProcessedData.backImage) } - if(ProcessedData.faceImage != null){ + if (ProcessedData.faceImage != null) { imgFaceViewer.setImageBitmap(ProcessedData.faceImage) } - if(ProcessedData.capturedFaceImage != null){ + if (ProcessedData.capturedFaceImage != null) { imgCapturedFaceViewer.setImageBitmap(ProcessedData.capturedFaceImage) } - if(ProcessedData.signImage != null){ + if (ProcessedData.signImage != null) { imgSignatureViewer.setImageBitmap(ProcessedData.signImage) } - if(ProcessedData.formattedString != null){ + if (ProcessedData.formattedString != null) { textViewCardInfo.text = ProcessedData.formattedString } @@ -69,11 +69,11 @@ class ResultActivity : AppCompatActivity() { private fun formatDateForNfc(date: String) : String { var pattern = Regex("[0-9]{2}-[0-9]{2}-[0-9]{4}") var out = "" - if(pattern.matches(date)) { + if (pattern.matches(date)) { out = date.substring(8,10) + date.substring(0,2) + date.substring(3,5) } pattern = Regex("[0-9]{2}-[0-9]{2}-[0-9]{2}") - if(pattern.matches(date)) { + if (pattern.matches(date)) { out = date.substring(6,8) + date.substring(0,2) + date.substring(3,5) } return out diff --git a/app/src/main/java/com/acuant/sampleapp/utils/CommonUtils.java b/app/src/main/java/com/acuant/sampleapp/utils/CommonUtils.java index 35a0100..57dfa75 100644 --- a/app/src/main/java/com/acuant/sampleapp/utils/CommonUtils.java +++ b/app/src/main/java/com/acuant/sampleapp/utils/CommonUtils.java @@ -1,12 +1,17 @@ package com.acuant.sampleapp.utils; -import com.acuant.acuantdocumentprocessing.resultmodel.HealthInsuranceCardResult; +import com.acuant.acuantdocumentprocessing.healthinsuranceresultmodel.HealthInsuranceCardResult; + import java.lang.reflect.Field; import java.util.Objects; public class CommonUtils { + + //This method is just a quick example of how to get some of the basic info. There are many + // fields that would not be covered by this. In a real implementations you should pick each + // field individually public static String stringFromHealthInsuranceResult(HealthInsuranceCardResult result){ - String str = ""; + StringBuilder str = new StringBuilder(); Field [] allFields = null; if (result != null) { allFields = HealthInsuranceCardResult.class.getDeclaredFields(); @@ -15,10 +20,10 @@ public static String stringFromHealthInsuranceResult(HealthInsuranceCardResult r for (Field field : allFields) { try { field.getName(); - if (!field.getName().startsWith("kCard") && !field.getName().startsWith("kDriversCard") && !field.getName().startsWith("kAuth") && String.class.isAssignableFrom(field.getType())) { + if (!field.getName().startsWith("rawText") && !field.getName().startsWith("frontImageString") && !field.getName().startsWith("backImageString") && String.class.isAssignableFrom(field.getType())) { field.setAccessible(true); if (field.get(result) != null && Objects.requireNonNull(field.get(result)).toString().trim().length()>0) { - str = str + field.getName() + ":" + field.get(result) + System.lineSeparator(); + str.append(field.getName()).append(": ").append(field.get(result)).append(System.lineSeparator()); } } } catch (IllegalAccessException e) { @@ -26,6 +31,6 @@ public static String stringFromHealthInsuranceResult(HealthInsuranceCardResult r } } } - return str; + return str.toString(); } } diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index a8cc384..24b86e8 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -9,7 +9,8 @@ ALINEAR ALINEAR Y TOCAR MUÉVETE MAS CERCA - DEMASIADO CERCA! + DEMASIADO CERCA + FUERA DE CUADRO MANTENER ESTABLE CAPTURA diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml deleted file mode 100644 index e155fff..0000000 --- a/app/src/main/res/values-fil/strings.xml +++ /dev/null @@ -1,95 +0,0 @@ - - SampleApp - - - Info - This sample needs camera permission. - This device does not support Camera2 API. - - ITAPAT SA ID - ITAPAT AT I-TAP - ILAPIT ANG CAMERA - MASYADONG MALAPIT! - HUWAG IGALAW - KINUKUHA - - Sinusuri ang MRZ - Itapat sa pasaporte - Ilapit sa pasaporte - Muling ihanay - Tapos na ang analisis - - Kinukuha... - Ihanay ang barcode - - - Itapat ang mukha\nupang magsimula - Masyadong malapit!\nIlayo ang mukha - Ilapit ang mukha sa screen - May anggulo ang mukha.\nHuwag ikiling - Itapat ang mukha\n sa frame - Huwag igalaw - Ikurap ang iyong mata - Kinukuha… - - Kinukuha\n%d… - Kinukuha\n%d… - - - - fil - Itapat ang mukha sa oblong - Punan ang oblong ng iyong mukha - Ilagay ang mukha sa frame - Nagkokonekta - I-tap ang screen para magsimula - Ilapit ang mukha sa screen - Masyadong maliwanag - Nagsimula na ang streaming… - Streaming, mabagal ang network… - Sinusuri… - Kinikilala ang mukha… - Kinukumpirma ang pagkakakilanlan - Sinusuri ang presenya… - Sinusuri ang liveness… - Loading… - Naglilikha ng pagkakakilanlan… - Sinusuri ang mukha… - Authenticate - Enrol - %1$s to %2$s - Masyadong malapit ang mukha - Sorry, hindi tiyak ang resulta - Huwag gumalaw - Maliwanag ang paligid or madilim ang screen - Maliwanag ang ilaw sa likuran - Ang iyong paligid ay madilim - Masyadong maliwanag ang iyong mukha - Huwag magsalita habang kinukuha ang mukha - Sorry, nag-time out ang sesyon - %1$s as %2$s to %3$s - Huwag ikiling ang ulo - Huwag ikiling ang ulo - Iharap ang ulo konti sa kanan - Iharap ang ulo konti sa kaliwa - Ihanay ang device sa antas ng mata - Ihanay ang device sa antas ng mata - Tapos na ang scan - Humanda… - Loading… - Camera error - Camera permission denied - Sorry, your device is currently not supported - Face detector error - An existing capture is already in progress - - - OK - Access to the camera is needed for detection - This application cannot run because it does not have the camera permission. The application will now exit. - Face detector dependencies cannot be downloaded due to low device storage - Could not classify the document - - Kuhanan ng larawan ang likod ng ID - Kuhanan ng larawan ang likod ng health insurance ID - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 395d6ae..89c1f02 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,7 +12,8 @@ ALIGN ALIGN AND TAP MOVE CLOSER - TOO CLOSE! + TOO CLOSE + NOT IN FRAME HOLD STEADY CAPTURING diff --git a/build.gradle b/build.gradle index f2c15cf..c8dc20f 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.4' + classpath 'com.android.tools.build:gradle:7.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' diff --git a/docs/MigrationDetails.md b/docs/MigrationDetails.md index ec6e35e..0958570 100644 --- a/docs/MigrationDetails.md +++ b/docs/MigrationDetails.md @@ -1,4 +1,472 @@ # Migration Details +---------- +## v11.6.0 + +**February 2023** + +### Updated String Keys + +One camera string key has been removed, and two keys have been added. Localizations need to add the following two keys to maintain full localization: + + TOO CLOSE + NOT IN FRAME + +### Changes to Document Camera and Image Evaluation + +This release includes changes to the way images are returned from the camera. These changes affect all implementations. In the past, the camera returned a path to a cache file that contained the image and any metadata. EvaluateImage created a similar cache to store the cropped image and metadata. Although cache images are regularly cleared out by the OS, the images could still last longer than needed. The SDK code could not delete the cached images because they had to remain for an arbitrary amount of time based on client workflow, and most implementations did not manually clear the cache or call the delete method provided with `AcuantImage`. + +Certain operations require us to cache the image, so cached images are still in use. With this release, cached images are created and deleted internally with the implementer now receiving a reference to a byte array that contains the data, which was previously contained in the file. As a result, there is less time for an interruption to the application that could leave images in the internal cache. However, because caches are still used (although for a much briefer period of time), Acuant still recommends manually clearing the cache when the application is paused, destroyed, or otherwise interrupted. + +Changes necessary to maintain the current workflow are as follows: + +Replace + + val url = data?.getStringExtra(ACUANT_EXTRA_IMAGE_URL) + +with + + val bytes = AcuantCameraActivity.getLatestCapturedBytes(clearBytesAfterRead = true) + +Because the bytes of the image would be too large to pass as part of a parcel, they are accessed through a static method in `AcuantCameraActivity`. Acuant recommends, for security and memory use, to set`clearBytesAfterRead` to true. + + CroppingData(imageUrlString: String) + +has been replaced with + + CroppingData(imageBytes: ByteArray) + +### Updated models returned by web calls + +The Document, Classification, Health Insurance, and Passive Liveness result models have been updated. These models are now in Kotlin resulting in more explicit nullability. Fields that are nullable in the web request are nullable in the Kotlin model, while those that are not nullable in the web request are not nullable in the Kotlin model. The structure of these models has been modified to more closely match the structure of the model returned through the web call. This modification reduces ambiguity and enables future fields in the web model to be mapped more easily to the Kotlin model. Fields also will more closely match their type (int to int, boolean to boolean, etc.) whereas, in the past, most fields were read as strings. Fields that represent an emun are now parsed into the appropriate enum. The unparsed value is still exposed if a new enum value is added in the web result and not yet mapped to the Kotlin model. + +Breakdowns of the new Kotlin models are shown below. Although these models might seem overwhelming, large portions remain unchanged from the previous version. + +Document + Classification: + + IDResult + val alerts: List + val unparsedAuthenticationSensitivity: Int + val authenticationSensitivity: AuthenticationSensitivity? + val biographic: DocumentBiographic? + val classification: Classification? + val dataFields: List + val device: Device? + val engineVersion: String? + val fields: List + val images: List + val instanceID: String + val libraryVersion: String? + val unparsedProcessMode: Int + val processMode: DocumentProcessMode? + val regions: List + val unparsedResult: Int + val result: AuthenticationResult? + val subscription: Subscription? + val unparsedTamperResult: Int + val tamperResult: AuthenticationResult? + val unparsedTamperSensitivity: Int + val tamperSensitivity: AuthenticationSensitivity? + + DocumentAlert + val actions: String? + val description: String? + val disposition: String? + val id: String + val information: String? + val key: String? + val model: String? + val name: String? + val unparsedResult: Int + val result: AuthenticationResult? + + DocumentBiographic + val age: Int + val birthDate: String? + val expirationDate: String? + val fullName: String? + val unparsedGender + val gender: GenderType? + val photo: String? + + Classification + val unparsedMode: Int + val mode: ClassificationMode? + val orientationChanged: Boolean + val presentationChanged: Boolean + val classificationDetails: ClassificationDetails? + val type: DocumentType? + + ClassificationDetails + val front: DocumentType? + val back: DocumentType? + + DocumentType + val unparsedDocumentClass: Int + val documentClass: DocumentClass? + val classCode: String? + val className: String? + val countryCode: String? + val unparsedDocumentDataTypes: List + val documentDataTypes: List + val geographicRegions: List + val id: String + val isGeneric: Boolean + val issue: String? + val issueType: String? + val issuerCode: String? + val issuerName: String? + val unparsedIssuerType: Int + val issuerType: IssuerType? + val keesingCode: String? + val name: String? + val unparsedReferenceDocumentDataTypes: List + val referenceDocumentDataTypes: List + val unparsedSize: Int + val size: DocumentSize? + val supportedImages: List + + DocumentImageType + val unparsedLight: Int + val light: LightSource? + val unparsedSide: Int + val side: DocumentSide? + + DocumentDataField + val unparsedDataSource: Int + val dataSource: DocumentDataSource? + val description: String? + val id: String + val isImage: Boolean + val key: String? + val name: String? + val regionOfInterest: Rectangle + val regionReference: String + val reliability: Double + val type: String? + val value: String? + + Rectangle + val height: Int + val width: Int + val x: Int + val y: Int + + Device + val hasContactlessChipReader: Boolean + val hasMagneticStripeReader: Boolean + val serialNumber: String? + val type: DeviceType? + + DeviceType + val manufacturer: String? + val model: String? + val unparsedSensorType: Int + val sensorType: SensorType? + + DocumentField + val dataFieldReferences: List + val unparsedDataSource: Int + val dataSource: DocumentDataSource? + val description: String? + val id: String + val isImage: Boolean + val key: String? + val name: String? + val regionReference: String + val type: String? + val value: String? + + DocumentImage + val glareMetric: Int? + val horizontalResolution: Int + val id: String + val isCropped: Boolean? + val isTampered: Boolean? + val unparsedLight: Int + val light: LightSource? + val mimeType: String? + val sharpnessMetric: Int? + val unparsedSide: Int + val side: DocumentSide? + val uri: String? + val verticalResolution: Int + + DocumentRegion + val unparsedDocumentElement: Int + val documentElement: DocumentElement? + val id: String + val imageReference: String + val key: String? + val rectangle: Rectangle + + Subscription + val unparsedDocumentProcessMode: Int + val documentProcessMode: DocumentProcessMode? + val id: String + val isActive: Boolean + val isDevelopment: Boolean + val isTrial: Boolean + val name: String? + val storePII: Boolean + +Document + Classification Enums: + + AuthenticationResult + Unknown(0), + Passed(1), + Failed(2), + Skipped(3), + Caution(4), + Attention(5) + + AuthenticationSensitivity + Normal(0), + High(1), + Low(2) + + ClassificationMode + Automatic(0), + Manual(1) + + DocumentClass + Unknown(0), + Passport(1), + Visa(2), + DriversLicense(3), + IdentificationCard(4), + Permit(5), + Currency(6), + ResidenceDocument(7), + TravelDocument(8), + BirthCertificate(9), + VehicleRegistration(10), + Other(11), + WeaponLicense(12), + TribalIdentification(13), + VoterIdentification(14), + Military(15), + ConsularIdentification(16) + + DocumentDataSource + None(0), + Barcode1D(1), + Barcode2D(2), + ContactlessChip(3), + MachineReadableZone(4), + MagneticStripe(5), + VisualInspectionZone(6), + Other(7) + + DocumentDataType + Barcode2D(0), + MachineReadableZone(1), + MagneticStripe(2) + + DocumentElement + Unknown(0), + None(1), + Photo(2), + Data(3), + Substrate(4), + Overlay(5) + + DocumentProcessMode + Default(0), + CaptureData(1), + Authenticate(2), + Barcode(3) + + DocumentSide + Front(0), + Back(1) + + DocumentSize + Unknown(0), + ID1(1), + ID2(2), + ID3(3), + Letter(4), + CheckCurrency(5), + Custom(6) + + GenderType + Unspecified(0), + Male(1), + Female(2), + Unknown(3) + + IssuerType + Unknown(0), + Country(1), + StateProvince(2), + Tribal(3), + Municipality(4), + Business(5), + Other(6) + + LightSource + White(0), + NearInfrared(1), + UltravioletA(2), + CoaxialWhite(3), + CoaxialNearInfrared(4) + + SensorType + Unknown(0), + Camera(1), + Scanner(2), + Mobile(3) + +Additionally `CardSide` was removed from `AcuantCommon` in favor of `DocumentSide` in `AcuantDocumentProcessing` + +Health Insurance: + + HealthInsuranceCardResult + var instanceID: String + val copayEr: String? + val copayOv: String? + val copaySp: String? + val copayUc: String? + val coverage: String? + val contractCode: String? + val dateOfBirth: String? + val deductible: String? + val effectiveDate: String? + val employer: String? + val expirationDate: String? + val firstName: String? + val groupName: String? + val groupNumber: String? + val issuerNumber: String? + val lastName: String? + val memberId: String? + val memberName: String? + val middleName: String? + val namePrefix: String? + val nameSuffix: String? + val other: String? + val payerId: String? + val planAdmin: String? + val planProvider: String? + val planType: String? + val frontImage: Bitmap? + val rawText: String? + val rxBin: String? + val rxGroup: String? + val rxId: String? + val rxPcn: String? + val backImage: Bitmap? + val listAddress: List
+ val listPlanCode: List + val listTelephone: List + val listEmail: List + val listWeb: List) + val transactionTimestamp: String? + + Address + val fullAddress: String? + val street: String? + val city: String? + val state: String? + val zip: String? + + PlanCode + val planCode: String? + + Telephone + val label: String? + val value: String? + + Email + val label: String? + val value: String? + + WebAddress + val label: String? + val value: String? + +Passive Liveness: + + PassiveLivenessResult + val livenessResult: LivenessResult? + val transactionId: String? + val errorDesc: String? + val unparsedErrorCode: String? + val errorCode: PassiveLivenessErrorCode? + + LivenessResult + val unparsedLivenessAssessment: String? + val livenessAssessment: LivenessAssessment? + val score: Int + +Passive Liveness Enums: + + LivenessAssessment + Error, + PoorQuality, + Live, + NotLive + + PassiveLivenessErrorCode + Unknown, + FaceTooClose, + FaceNotFound, + FaceTooSmall, + FaceAngleTooLarge, + FailedToReadImage, + InvalidRequest, + InvalidRequestSettings, + Unauthorized, + NotFound + +### Notes on possible backwards incompatibility in Initialization + +This release contains minor changes to initialization that might cause backwards incompatibility in some implementations. Most implementations will not be affected by these changes. + +The changes are as follows: + +- The two main functions used to initialize the SDK, `initializeWithToken` and `initialize`, remain unchanged. The recommended function to create a `Credential` prior to initializing the SDK, `initFromXml` also remains unchanged. + +- The `Credential` and `Endpoint` classes were migrated to Kotlin. This means that fields that might have been nullable in the past might no longer be nullable. Some fields in the credential that used to be freely modifiable are now vals (set once during the creation of the object). Most implementations do not set or access values from these classes and, therefore, should be unaffected. + +- The names of some endpoints inside the `Endpoint` class have changed. The old names have been left accessible with a deprecated flag. The names of the endpoints within the XML file remain unchanged. Most implementations do not need to set or access these values. + +- The `Credential` class used to have many manual overloads of the static `init` and `initWithToken` methods. These methods were combined into singular Kotlin functions with some default parameters. Implementations that load the credential from an XML file (the recommended workflow) will be unaffected by this change. Implementations that load the credential using one of these two methods should be reviewed because the signatures have changed. In Kotlin implementations it is encouraged to explicitly set parameter names within the calls to the respective functions. The new signatures of the functions are shown below. + + fun init( + username: String, + password: String, + subscription: String?, + acasEndpoint: String, + assureIdEndpoint: String? = null, + frmEndpoint: String? = null, + passiveLivenessEndpoint: String? = null, + ipLivenessEndpoint: String? = null, + ozoneEndpoint: String? = null, + healthInsuranceEndpoint: String? = null + ) + + fun initWithToken( + token: String, + acasEndpoint: String, + assureIdEndpoint: String? = null, + frmEndpoint: String? = null, + passiveLivenessEndpoint: String? = null, + ipLivenessEndpoint: String? = null, + ozoneEndpoint: String? = null, + healthInsuranceEndpoint: String? = null + ) + +- The `parseXml` method in `AcuantInitializer` has been made internal. Most implementations do not use this method. The recommended replacement for it (if in use) is the previously existing static `initFromXml` within the `Credential` class. + +- The list of packages to initialize no longer needs to contains packages that don't have content to initialize. `EchipInitializer` and `ImageProcessorInitializer` are no longer needed because those packages will now just check the initialization state of the active credential. The `MrzCameraInitializer` is still required (if MRZ reading is in use) because that class loads OCR data. + +### Initialization Behavioral changes + +- Previously all methods for creating the Credential and initializing the SDK would function only once. Future calls to these methods would be ignored. + +- Now the Credential can be recreated at will and used to reinitialize the SDK. This allows use-cases where the implementer wants to swap credentials or endpoints after having performed one or more workflows. + ---------- ## v11.5.0 @@ -10,7 +478,7 @@ - Instances in which credentials had to be specified by the implementer have been removed. The SDK will always use the result of Credential.get(). -- The Error class in AcuantCommon has been renamed to AcuantError. The renaming helps prevent confusion between `kotlin.Error`, `java.lang.Error`, and `acuant.common.model.Error` because IDEs would always default to the first or second. Additionally, AcuantError now contains and additionalDetails field. This nullable field will in some circumstances contain information that is helpful in debugging, such as a stack trace or server response code, but follows no standard format. Error codes for network requests have been simplified to `ERROR_Network`, whereas previously, there were approximately 20 error codes that all meant the network request failed. +- The Error class in AcuantCommon has been renamed to AcuantError. The renaming helps prevent confusion between `kotlin.Error`, `java.lang.Error`, and `acuant.common.model.Error` because IDEs would always default to the first or second. Additionally, AcuantError now contains an additionalDetails field. This nullable field will in some circumstances contain information that is helpful in debugging, such as a stack trace or server response code, but follows no standard format. Error codes for network requests have been simplified to `ERROR_Network`, whereas previously, there were approximately 20 error codes that all meant the network request failed. class AcuantError( val errorCode: Int, diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b000d2d..99618d7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Dec 07 11:00:37 PST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME