Skip to content

Instantly share code, notes, and snippets.

@RobertApikyan
Created January 27, 2021 07:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RobertApikyan/3ca9afc1fb2b778432f1cdd1b4fef299 to your computer and use it in GitHub Desktop.
Save RobertApikyan/3ca9afc1fb2b778432f1cdd1b4fef299 to your computer and use it in GitHub Desktop.
package com.systech.core_v2.auth
import android.content.Context
import android.graphics.*
import android.graphics.ImageFormat.NV21
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.hardware.Sensor
import android.hardware.SensorManager
import android.hardware.SensorManager.SENSOR_DELAY_UI
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraMetadata.*
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureResult
import android.hardware.camera2.TotalCaptureResult
import android.location.Location
import android.location.LocationManager
import android.os.Build
import android.os.Environment.DIRECTORY_PICTURES
import android.util.Log
import android.util.Size
import android.util.SizeF
import android.view.View
import android.view.ViewGroup
import android.view.ViewStub
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.camera2.interop.Camera2Interop
import androidx.camera.core.*
import androidx.camera.core.Camera
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import com.google.android.gms.vision.barcode.Barcode
import com.google.android.gms.vision.barcode.Barcode.DATA_MATRIX
import com.google.android.gms.vision.barcode.Barcode.QR_CODE
import com.google.gson.Gson
import com.google.gson.internal.LinkedTreeMap
import com.systech.UniSecureSDK.efp.FingerprintFusionManager
import com.systech.UniSecureSDK.efp.FingerprintFusionManagerResult
import com.systech.camerax.v1.OpenGLRenderer
import com.systech.camerax.v1.TextureViewRenderSurface
import com.systech.core.BuildConfig
import com.systech.core.R
import com.systech.core.auth.legacy.AuthenticationResponse
import com.systech.core.data.pref.Preferences
import com.systech.core.data.repository.productFamily.legacy.ProductFamilyInfo
import com.systech.core.di.Injections
import com.systech.core.domain.fetcher.result_listener.resultListener
import com.systech.core.domain.manager.SECSL_V1_DataSetupManager
import com.systech.core.util.AUTHORIZATION_ERROR_CODE
import com.systech.core.util.readAsString
import com.systech.core.util.runOnUiThread
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.*
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import javax.inject.Inject
import kotlin.collections.ArrayList
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
import kotlin.properties.Delegates
/** Helper type alias used for analysis use case callbacks */
private typealias FrameAnalysisListener = (result: CaptureImageAnalyzerResult?) -> Unit
internal typealias performFPFusionAndAuthenticationCallback = (result: AuthenticationResponse?, errorMessage: String?) -> Unit
class AuthView internal constructor() : LifecycleObserver {
private var preview: Preview? = null
private lateinit var glPreviewRenderer: OpenGLRenderer
private lateinit var viewFinder: View
private var camera: Camera? = null
private var imageAnalyzer: ImageAnalysis? = null
private var imageCapture: ImageCapture? = null
private var authListener: AuthListener by Delegates.notNull()
private lateinit var cameraExecutor: ExecutorService
private lateinit var context: Context
private val isCalibrationBusy = AtomicBoolean(false)
private val isCapturePictureBusy = AtomicBoolean(false)
private var takePictureAtomicCounter = AtomicInteger(0)
@Inject
internal lateinit var decoder: BarcodeDecoderV2
@Inject
internal lateinit var pfi: ProductFamilyInfo
@Inject
internal lateinit var preferences: Preferences
@Inject
internal lateinit var dataSetupManager: SECSL_V1_DataSetupManager
private var serverUrl: String = ""
private var userName: String = ""
private var password: String = ""
private var location = Location(LocationManager.NETWORK_PROVIDER)
private var fingerprintManager: FingerprintFusionManager? = null
private var activeCameraProvider: ProcessCameraProvider? = null
private lateinit var sensorManager: SensorManager
private lateinit var accelerometerSensor: Sensor
@Inject
internal lateinit var stabilitySensorListener: StabilitySensorListener
var shouldAutoResume: Boolean = false
var isCapturing: Boolean = false
private set
var isCameraStarting: Boolean = false
private set
var saveImages: Boolean = false
var saveImageVerbose: Boolean = false
var isScanOnly: Boolean = false
val isDeviceCalibrated: Boolean
get() {
return deviceCalibration != null
}
private var deviceCalibration: DeviceCalibrationResult? = null
private data class CapturedImageData(
val imagePath: String,
val cornerPoints: Array<PointF>,
val totalCaptureTime: Long
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CapturedImageData
if (imagePath != other.imagePath) return false
if (!cornerPoints.contentEquals(other.cornerPoints)) return false
return true
}
override fun hashCode(): Int {
var result = imagePath.hashCode()
result = 31 * result + cornerPoints.contentHashCode()
return result
}
}
/**
* @hide
* @suppress
*/
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun destroy() {
Log.d(TAG, "destroy: Shutting down AuthView_v2 OpenGL Renderer...")
glPreviewRenderer.shutdown()
Log.d(TAG, "destroy: Shutdown AuthView_v2 OpenGL Renderer complete.")
}
/**
* @hide
* @suppress
*/
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun pause() {
this.cleanupResourcesOnPause()
}
/**
* @hide
* @suppress
*/
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun resume() {
if (this.activeCameraProvider == null
&& this.shouldAutoResume
&& !this.isCapturing) {
if (this.isDeviceCalibrated) {
this.startCamera()
}
else {
this.startCalibration()
}
}
}
/**
* The init method sets the server information required for sending authentication.
*
* @param parentView A ViewGroup to add AuthView inside it
* @param url String that specifies the URL of the server
* @param username String that specifies the username to be used for authentication
* @param password String that specifies the password to be used for authentication *
* @param enableAlternativeDecoder Enable Alternate decoder inorder to test ZXing decoder
* @param authListener The callback for AuthView
* @see AuthListener
*
*/
@JvmOverloads
fun init(parentView: ViewGroup, serverUrl: String, username: String,
password: String, authListener: AuthListener, enableAlternativeDecoder: Boolean = false) {
Injections.buildAuthComponent().inject(this)
this.serverUrl = serverUrl
this.userName = username
this.password = password
this.shouldAutoResume = false
this.authListener = authListener
this.context = parentView.context
this.decoder.forceAlternateDecoder = enableAlternativeDecoder
val inflatedView = View.inflate(this.context, R.layout.authview_v2, parentView)
val viewFinderStub = inflatedView.findViewById<ViewStub>(R.id.viewFinderStub)
val renderer = OpenGLRenderer().also { glPreviewRenderer = it }
viewFinder = TextureViewRenderSurface.inflateWith(viewFinderStub, renderer)
parentView.setOnClickListener {
authListener.onAuthViewClick()
}
cameraExecutor = Executors.newSingleThreadExecutor()
this.sensorManager = this.context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
this.accelerometerSensor = this.sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
parentView.post {
GlobalScope.launch {
this@AuthView.deviceCalibration = getDeviceCalibration(context, dataSetupManager)
runOnUiThread {
this@AuthView.authListener.onAuthReady()
if(this@AuthView.isDeviceCalibrated) {
this@AuthView.startCamera()
}
// Register lifecycle observer after the view has initialized.
(context as AppCompatActivity).lifecycle.addObserver(this@AuthView)
}
}
}
}
/**
* The setLocationWithLatitude method sets the location coordinates to associate with the authentication request.
* If location services are disabled or not used,
* a string value of 0 should be sent for both latitude and longitude parameters.
* @param location Which specifies the coordinates gathered from the mobile application
* @see Location
*
*/
fun updateLocation(location: Location) {
this.location = location
}
/**
* The restartScanning method restarts the scanning
*/
fun restartScanning() {
this.cleanupResourcesOnPause()
this.shouldAutoResume = true
this.resume()
}
/**
* The authenticateItem method sends the provided item fingerprint data to the server for authentication.
* @param captureData item fp data as captured and provided by the AuthView
* @see AuthListener.captureCompletedSuccessfully
*/
fun authenticateItem(captureData: ItemCaptureData) {
captureData.productIdentifier?.let { productId ->
pfi.setValuesForCode(productId)
val isSerialized = pfi.getSerialized(productId)
val profileId = pfi.getProfileID(productId)
val productItemId = pfi.getCurrentItemID(productId)
val authPkId = pfi.getPkid(productId)
val bcLabel: String = if (captureData.needsUPCPrefix()) {
"UPC${captureData.sendId}"
} else {
captureData.sendId
}
val wPath = if (isSerialized) {
"/fingerprint/api/verification/?format=json"
} else {
"/fingerprint/api/non-serial-verification/?format=json"
}
val uploadRequest = UniSecureSDK.createAuthenticateFPRequest(
captureData.fusedFP,
captureData.imgBytes,
bcLabel,
location.latitude,
location.longitude,
profileId,
productItemId,
serverUrl,
userName,
password,
wPath,
preferences.deviceRegistrationID,
context.applicationContext)
sendAuthenticationRequest(uploadRequest, authPkId, captureData.captureTime)
}
}
/**
* Send a request for Serial Only Verification.
* @param productId The product ID of the item to authenticate.
* @param decodedValue The decodedValue of the barcode to authenticate.
* @see AuthListener.captureCompletedForSerialVerification
*/
fun performSerialOnlyAuthentication(productId: String, decodedValue: String) {
pfi.setValuesForCode(productId)
val profileId = pfi.getProfileID(productId)
val productItemId = pfi.getCurrentItemID(productId)
val authPkId = pfi.getPkid(productId)
val uploadRequest = UniSecureSDK.createAuthenticateSerialOnlyRequest(
imageFileName = decodedValue,
latitude = location.latitude,
longitude = location.longitude,
profileID = profileId,
productItemID = productItemId,
serverURL = serverUrl,
serverUsername = userName,
serverPassword = password,
uploadPath = "/fingerprint/api/verification/?format=text",
deviceUUID = preferences.deviceRegistrationID,
context = context.applicationContext)
sendAuthenticationRequest(uploadRequest, authPkId, 0.0)
}
/**
* Call the method to invoke device calibration.
* Calibration is required before a device can be used for authentication.
* @see AuthView.isDeviceCalibrated to determine if device is calibrated
*/
@Synchronized fun startCalibration() {
if (this.isCameraStarting) {
return
}
this.isCameraStarting = true
this.sensorManager.registerListener(this.stabilitySensorListener, this.accelerometerSensor, SENSOR_DELAY_UI)
if (this.isDeviceCalibrated) {
runOnUiThread {
this.authListener.showProgressBar()
this.cleanupResourcesOnPause()
this.shouldAutoResume = false
authListener.deviceCalibrationSuccessful(this.deviceCalibration!!)
}
this.isCameraStarting = false
return
}
this.shouldAutoResume = true
decoder.resetDecodeMode()
(this.context as AppCompatActivity).window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
this.fingerprintManager?.close()
this.fingerprintManager = null
val cameraProviderFuture = ProcessCameraProvider.getInstance(this.context)
cameraProviderFuture.addListener(Runnable {
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
this.activeCameraProvider = cameraProvider
val screenAspectRatio = AspectRatio.RATIO_4_3
// Preview
val previewBuilder = Preview.Builder()
.setTargetAspectRatio(screenAspectRatio)
Camera2Interop.Extender(previewBuilder).setSessionCaptureCallback(
object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest, result: TotalCaptureResult
) {
val lensState = result.get(CaptureResult.LENS_STATE)
?: LENS_STATE_STATIONARY
val afState = result.get(CaptureResult.CONTROL_AF_STATE)
?: CONTROL_AF_STATE_PASSIVE_FOCUSED
val aeState = result.get(CaptureResult.CONTROL_AE_STATE)
?: CONTROL_AE_STATE_CONVERGED
isFocused = lensState == LENS_STATE_STATIONARY
&& (afState == CONTROL_AF_STATE_INACTIVE || afState == CONTROL_AF_STATE_PASSIVE_FOCUSED)
&& aeState == CONTROL_AE_STATE_CONVERGED
// Log.d(TAG, "onCaptureCompleted: isFocused=$isFocused afState=$afState aeState=$aeState")
}
})
this.preview = previewBuilder
.build().also {
glPreviewRenderer.attachInputPreview(it)
}
// Select back camera
val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
val rotation = this.viewFinder.display.rotation
val analysisData: MutableMap<Int, MutableList<Float>> = HashMap()
val picturesDirectory = this.context.getExternalFilesDir(DIRECTORY_PICTURES)
val saveImageDirectory = File(picturesDirectory,
SimpleDateFormat(FILENAME_FORMAT,
Locale.US
).format(System.currentTimeMillis()))
var lastDecodedSendId = ""
var lastSentStepNumber = -1
var imageCounter = 0
var totalHighResCaptureTime = 0.0
var totalEfpProcessingTime = 0.0
var totalCaptureCounter = 0
// ImageAnalysis
imageAnalyzer = ImageAnalysis.Builder()
// .setTargetAspectRatio(screenAspectRatio)
.setTargetResolution(PREVIEW_TARGET_RESOLUTION)
// Set initial target rotation, we will have to call this again if rotation changes
// during the lifecycle of this use case
.setTargetRotation(rotation)
.build()
// The analyzer can then be assigned to the instance
.also {
val calibrationStartTime = System.currentTimeMillis()
resetDecodeProfiler()
var lastDecodeTime = System.currentTimeMillis()
it.setAnalyzer(cameraExecutor, CaptureImageAnalyzer(decoder, pfi) { result ->
if (deviceCalibration != null) {
return@CaptureImageAnalyzer
}
result?.let { decodeResult ->
lastDecodeTime = System.currentTimeMillis()
if (decodeResult.status == CaptureImageAnalyzerResultStatus.ProductNotFound) {
runOnUiThread {
this.authListener.setCaptureMessageText(this.context.getString(R.string.unknown_product_title))
}
} else {
runOnUiThread {
when (decodeResult.status) {
CaptureImageAnalyzerResultStatus.TooFar -> {
this.authListener.setCaptureMessageText(this.context.getString(R.string.closer))
}
CaptureImageAnalyzerResultStatus.TooClose -> {
this.authListener.setCaptureMessageText(this.context.getString(R.string.farther))
}
CaptureImageAnalyzerResultStatus.Rotate90 -> {
this.authListener.setCaptureMessageText(this.context.getString(R.string.rotate90))
}
CaptureImageAnalyzerResultStatus.ReadyToCapture -> {
if (this.stabilitySensorListener.isSteady) {
this.authListener.setCaptureMessageText(this.context.getString(R.string.calibrating_hold_steady))
} else {
this.authListener.setCaptureMessageText(this.context.getString(R.string.capturing_motion_detected))
}
}
else -> {
TODO()
}
}
}
if (analysisData.count() > 0
&& lastDecodedSendId != decodeResult.sendId) {
// product ID changed user probably started scanning a different barcode.
// restart the calibration process.
resetDecodeProfiler()
analysisData.clear()
currentAnalysisDPI = CALIBRATION_MIN_DPI_AT_1080P
this.fingerprintManager?.close()
this.fingerprintManager = null
}
lastDecodedSendId = decodeResult.sendId
if (decodeResult.status == CaptureImageAnalyzerResultStatus.ReadyToCapture
&& this.stabilitySensorListener.isSteady) {
this.takePicture(CALIBRATION_MAX_TAKE_PICTURE_QUEUE_LENGTH,
previewCornerPoints = decodeResult.cornerPoints,
takePictureStartingListener = {
val currentStep = if (analysisData[decodeResult.analysisDPI] != null) {
analysisData[decodeResult.analysisDPI]!!.size + 1
} else {
1
}
if (currentStep != lastSentStepNumber) {
lastSentStepNumber = currentStep
runOnUiThread {
this.authListener.onCalibrationProgressStepStarting(decodeResult.analysisDPI, currentStep, CALIBRATION_FRAME_COUNT)
}
}
}) { captureData, skippedCapture ->
if (!isCalibrationBusy.compareAndSet(false, true)) {
// Log.d(TAG, "startCalibration: Skipping calibration due to lock on stepNumber->$lastSentStepNumber analysisDPI->${decodeResult.analysisDPI} currentDPI->$currentAnalysisDPI")
captureData?.let { l_data ->
File(l_data.imagePath).delete()
}
return@takePicture
}
// Log.d(TAG, "startCalibration: Locking calibration ${decodeResult.analysisDPI} $currentAnalysisDPI")
if (decodeResult.analysisDPI != currentAnalysisDPI) {
// Log.d(TAG, "startCalibration: Unlocking calibration DPI Mismatch: ${decodeResult.analysisDPI} $currentAnalysisDPI")
captureData?.let { l_data ->
File(l_data.imagePath).delete()
}
isCalibrationBusy.set(false)
return@takePicture
}
if (!isCapturing) {
// Log.d(TAG, "startCalibration: Unlocking calibration NotCapturing $currentAnalysisDPI")
captureData?.let { l_data ->
File(l_data.imagePath).delete()
}
isCalibrationBusy.set(false)
return@takePicture
}
if (!this.stabilitySensorListener.isSteady) {
// Log.d(TAG, "startCalibration: Unlocking calibration Camera not steady $currentAnalysisDPI")
runOnUiThread {
this.authListener.setCaptureMessageText(this.context.getString(R.string.capturing_motion_detected))
}
captureData?.let { l_data ->
File(l_data.imagePath).delete()
}
isCalibrationBusy.set(false)
return@takePicture
}
val currentStep = if (analysisData[decodeResult.analysisDPI] != null) {
analysisData[decodeResult.analysisDPI]!!.size + 1
} else {
1
}
if (captureData == null) {
if (!skippedCapture) {
runOnUiThread {
this.authListener.onCalibrationProgressStepCompleted(decodeResult.analysisDPI, currentStep, CALIBRATION_FRAME_COUNT, CaptureStepStatus.DecodeFailed, this.context.getString(R.string.calibrating_hold_steady))
}
}
// Log.d(TAG, "startCalibration: Unlocking calibration decodeFailed $currentAnalysisDPI")
isCalibrationBusy.set(false)
return@takePicture
}
val engineProcessStartTime = System.currentTimeMillis()
if (this.fingerprintManager == null) {
this.fingerprintManager = FingerprintFusionManager(this.context,
BuildConfig.DEBUG || BuildConfig.CAN_DECRYPT,
this.pfi.getJSONForProductID(decodeResult.productIdentifier, decodeResult.sendId, serverUrl, userName, password))
}
val efpCalibrationResult = this.fingerprintManager!!.getImageCalibrationResult(captureData.imagePath, captureData.cornerPoints, this.saveImages, decodeResult.needsUPCPrefix())
val engineProcessTime = System.currentTimeMillis() - engineProcessStartTime
// Log.d(TAG, "startCalibration: efpProcessingTime: ${engineProcessTime / 1000.0} seconds.")
totalHighResCaptureTime += captureData.totalCaptureTime
totalEfpProcessingTime += engineProcessTime
totalCaptureCounter += 1
imageCounter += 1
if (efpCalibrationResult.statusCode == 0) {
// Log.d(TAG, "startCalibration: dpi->${decodeResult.analysisDPI} focusScore->${efpCalibrationResult.focusScore} resultString->${efpCalibrationResult.resultString}")
// Add focusScore to existing analysis HashMap or create a new entry if one doesn't exists for this DPI
analysisData[decodeResult.analysisDPI]?.add(efpCalibrationResult.focusScore)
?: run {
analysisData[decodeResult.analysisDPI] = mutableListOf(efpCalibrationResult.focusScore)
}
val focusScoreArray = analysisData[decodeResult.analysisDPI]!!
if (saveImages) {
efpCalibrationResult.efpImagePath?.let { efpImagePath ->
saveImageDirectory.mkdirs()
val fileNamePrefix = "Calibration-${decodeResult.analysisDPI}#${"%02d".format(imageCounter)}#${"%03d".format(efpCalibrationResult.statusCode)}#${"%.4f".format(efpCalibrationResult.focusScore)}#${"%.4f".format(engineProcessTime / 1000.0)}"
if (saveImageVerbose) {
val captureFile = File(captureData.imagePath)
val saveImageFile = File(saveImageDirectory, "$fileNamePrefix.${captureFile.extension}")
captureFile.copyTo(saveImageFile, true)
val captureDataFile = File(saveImageDirectory, "$fileNamePrefix-captureData.json")
val captureDataString = Gson().toJson(captureData)
captureDataFile.writeText(captureDataString)
val decodedDataFile = File(saveImageDirectory, "$fileNamePrefix-decodeData.json")
val decodedDataString = Gson().toJson(decodeResult)
decodedDataFile.writeText(decodedDataString)
}
val efpImageFile = File(efpImagePath)
val saveEfpImageFile = File(saveImageDirectory, "$fileNamePrefix-efp.${efpImageFile.extension}")
efpImageFile.copyTo(saveEfpImageFile, true)
}
}
if (focusScoreArray.size >= CALIBRATION_FRAME_COUNT) {
// discard non related scores.
discardCalibratedUnRelatedValuesAtDPI(decodeResult.analysisDPI, analysisData)
// if we still have enough frames.
if (focusScoreArray.size >= CALIBRATION_FRAME_COUNT) {
// We have 6 or more focus scores at current DPI adjust the DPI
runOnUiThread {
this.authListener.onCalibrationProgressStepCompleted(decodeResult.analysisDPI, CALIBRATION_FRAME_COUNT, CALIBRATION_FRAME_COUNT, CaptureStepStatus.Success, this.context.getString(R.string.calibrating_hold_steady))
}
if (analysisData.keys.size > 2) {
var bestDPI = currentAnalysisDPI
var bestScore = 10000.0f
analysisData.keys.forEach { dpi ->
val medianScore = getCalibratedAverageValueAtDPI(dpi, analysisData)
// Log.d(TAG, "startCalibration: dpi->$dpi medianScore->${medianScore}")
if (medianScore != 0.0f
&& medianScore < bestScore) {
bestDPI = dpi
bestScore = medianScore
}
}
// Log.d(TAG, "startCalibration: Current BestDPI->$bestDPI")
if (bestDPI in CALIBRATION_MIN_DPI_AT_1080P until currentAnalysisDPI) {
// bestDPI is greater than Min Allowed DPI
// and score got worse as we moved closer
// we are basically getting out of focus
// because we are too close. Stop and
// use the best focused DPI
runOnUiThread {
this.authListener.showProgressBar()
this.cleanupResourcesOnPause()
this.shouldAutoResume = false
}
val currentTotalCalibrationTime = System.currentTimeMillis() - calibrationStartTime
val calibratedPair = getCalibratedDPIValue(analysisData)
val averageImageCaptureTime = totalHighResCaptureTime / totalCaptureCounter
val averageEfpProcessingTime = totalEfpProcessingTime / totalCaptureCounter
val averageDecoderFrameTime = if(decodeFrameCount.get() > 0) {
totalDecodeFrameTime.get().toDouble() / decodeFrameCount.get()
}
else {
0.0
}
val experienceScore = DeviceCalibrationResult.calculateExperienceScore(
averageImageCaptureTime,
averageEfpProcessingTime,
averageDecoderFrameTime)
val attemptedDPIs = analysisData.keys.sorted().map { dpiItem ->
"$dpiItem -> ${"%.4f".format(getCalibratedAverageValueAtDPI(dpiItem, analysisData))}" }
val calibrationResult = DeviceCalibrationResult(
version = DeviceCalibrationResult.EXPECTED_CALIBRATION_VERSION,
dpi = calibratedPair.first,
focusScore = calibratedPair.second,
productId = decodeResult.productIdentifier!!,
productName = pfi.getTitle(decodeResult.productIdentifier),
barcodeWidth = decodeResult.decodedSizeInMM.width,
barcodeHeight = decodeResult.decodedSizeInMM.height,
sdkVersion = "${BuildConfig.VERSION_NAME}-${BuildConfig.VERSION_CODE}",
osVersion = "Android OS: v${Build.VERSION.RELEASE} (SDK:${Build.VERSION.SDK_INT})",
calibrationTime = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).format(System.currentTimeMillis()),
calibratedBy = userName,
latitude = location.latitude,
longitude = location.longitude,
averageImageCaptureTime = averageImageCaptureTime,
averageEfpProcessingTime = averageEfpProcessingTime,
totalFrameCaptures = totalCaptureCounter,
totalCalibrationDuration = currentTotalCalibrationTime,
attemptedDPIs = attemptedDPIs.joinToString("\n"),
averageDecoderFrameTime = averageDecoderFrameTime,
totalDecoderFrameCount = decodeFrameCount.get(),
experienceScore = experienceScore
)
// Log.d(TAG, "startCalibration: calibratedDPI->$calibrationResult")
this.uploadDeviceCalibration(calibrationResult)
runOnUiThread {
this.deviceCalibration = calibrationResult
this.authListener.onCalibrationProgressStepCompleted(calibrationResult.dpi, CALIBRATION_FRAME_COUNT, CALIBRATION_FRAME_COUNT, CaptureStepStatus.Success, this.context.getString(R.string.calibrating_hold_steady))
this.authListener.deviceCalibrationSuccessful(calibrationResult)
}
} else {
imageCounter = 0
currentAnalysisDPI += 25
runOnUiThread {
this.authListener.onCalibrationProgressStepStarting(currentAnalysisDPI, 1, CALIBRATION_FRAME_COUNT)
}
}
} else {
imageCounter = 0
currentAnalysisDPI += 25
runOnUiThread {
this.authListener.onCalibrationProgressStepStarting(currentAnalysisDPI, 1, CALIBRATION_FRAME_COUNT)
}
}
} else {
runOnUiThread {
this.authListener.onCalibrationProgressStepStarting(decodeResult.analysisDPI, focusScoreArray.size + 1, CALIBRATION_FRAME_COUNT)
}
}
} else {
runOnUiThread {
this.authListener.onCalibrationProgressStepCompleted(decodeResult.analysisDPI, currentStep, CALIBRATION_FRAME_COUNT, CaptureStepStatus.Success, this.context.getString(R.string.calibrating_hold_steady))
}
}
} else {
Log.e(TAG, "startCalibration: efp Error: focusScore->${efpCalibrationResult.focusScore} ${efpCalibrationResult.statusCode} resultString->${efpCalibrationResult.resultString}")
if (saveImages) {
saveImageDirectory.mkdirs()
val fileNamePrefix = "Calibration-${decodeResult.analysisDPI}#${"%02d".format(imageCounter)}#${"%03d".format(efpCalibrationResult.statusCode)}#${"%.4f".format(efpCalibrationResult.focusScore)}#${"%.4f".format(engineProcessTime / 1000.0)}"
if (saveImageVerbose) {
val captureFile = File(captureData.imagePath)
val saveImageFile = File(saveImageDirectory, "$fileNamePrefix.${captureFile.extension}")
captureFile.copyTo(saveImageFile, true)
val captureDataFile = File(saveImageDirectory, "$fileNamePrefix-captureData.json")
val captureDataString = Gson().toJson(captureData)
captureDataFile.writeText(captureDataString)
val decodedDataFile = File(saveImageDirectory, "$fileNamePrefix-decodeData.json")
val decodedDataString = Gson().toJson(decodeResult)
decodedDataFile.writeText(decodedDataString)
}
efpCalibrationResult.efpImagePath?.let { efpImage ->
val efpImageFile = File(efpImage)
val saveEfpImageFile = File(saveImageDirectory, "$fileNamePrefix-efp.${efpImageFile.extension}")
efpImageFile.copyTo(saveEfpImageFile, true)
}
}
runOnUiThread {
val message = when (efpCalibrationResult.statusCode) {
EFP_REJECT_INSUFFICIENT_FOCUS_CODE -> {
this.context.getString(R.string.efp_reject_insufficient_focus)
}
EFP_REJECT_INVALID_BRIGHTNESS_CODE -> {
this.context.getString(R.string.efp_reject_unacceptable_brightness)
}
else -> {
this.context.getString(R.string.calibrating_hold_steady)
}
}
this.authListener.onCalibrationProgressStepCompleted(decodeResult.analysisDPI, currentStep, CALIBRATION_FRAME_COUNT, CaptureStepStatus.EngineRejected, message)
}
}
File(captureData.imagePath).delete()
efpCalibrationResult.efpImagePath?.let { efpImagePath ->
File(efpImagePath).delete()
}
// Log.d(TAG, "startCalibration: Unlocking calibration ${decodeResult.analysisDPI} $currentAnalysisDPI")
isCalibrationBusy.set(false)
}
}
}
}?:run {
val timeSinceLastDecode = System.currentTimeMillis() - lastDecodeTime
if (timeSinceLastDecode > NO_BARCODE_MAX_DECODE_TIME && this.isCapturing) {
// If no decode for 4 seconds reset the timer
// And raise a looking for barcode message.
lastDecodeTime = System.currentTimeMillis()
runOnUiThread {
this.authListener.setCaptureMessageText(this.context.getString(R.string.looking_for_barcode))
}
}
}
val currentTotalCalibrationTime = System.currentTimeMillis() - calibrationStartTime
if (currentTotalCalibrationTime > 4 * 60 * 1000) {
// If more than 4 minutes has passed in calibration,
// stop the calibration.
// Use the captured calibration values to
// determine the best DPI for the device.
runOnUiThread {
this.authListener.showProgressBar()
this.cleanupResourcesOnPause()
this.shouldAutoResume = false
this.deviceCalibration = null
this.authListener.deviceCalibrationFailed()
}
}
})
}
// ImageCapture
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
// We request aspect ratio but no resolution to match preview config, but letting
// CameraX optimize for whatever specific resolution best fits our use cases
.setTargetAspectRatio(screenAspectRatio)
// Set initial target rotation, we will have to call this again if rotation changes
// during the lifecycle of this use case
.setTargetRotation(rotation)
.build()
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
currentAnalysisDPI = CALIBRATION_MIN_DPI_AT_1080P
// Bind use cases to camera
camera = cameraProvider.bindToLifecycle(
(this.context as AppCompatActivity), cameraSelector, preview, imageAnalyzer, imageCapture)
this.isCapturing = true
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
this.isCameraStarting = false
}, ContextCompat.getMainExecutor((this.context as AppCompatActivity)))
}
@Synchronized private fun startCamera() {
if (this.isCameraStarting) {
return
}
this.isCameraStarting = true
this.sensorManager.registerListener(this.stabilitySensorListener, this.accelerometerSensor, SENSOR_DELAY_UI)
if (!this.isDeviceCalibrated) {
// Do not start camera is device is not calibrated
this.isCameraStarting = false
return
}
this.shouldAutoResume = true
decoder.resetDecodeMode()
(this.context as AppCompatActivity).window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
this.fingerprintManager?.close()
this.fingerprintManager = null
val cameraProviderFuture = ProcessCameraProvider.getInstance(this.context)
cameraProviderFuture.addListener(Runnable {
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
this.activeCameraProvider = cameraProvider
// Get screen metrics used to setup camera for full screen resolution
// val metrics = DisplayMetrics().also { this.scanningPreview!!.display.getRealMetrics(it) }
// Log.d(TAG, "Screen metrics: ${metrics.widthPixels} x ${metrics.heightPixels}")
// val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels)
val screenAspectRatio = AspectRatio.RATIO_4_3
// Log.d(TAG, "Preview aspect ratio: $screenAspectRatio")
// Preview
val previewBuilder = Preview.Builder()
.setTargetAspectRatio(screenAspectRatio)
Camera2Interop.Extender(previewBuilder).setSessionCaptureCallback(
object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest, result: TotalCaptureResult
) {
val lensState = result.get(CaptureResult.LENS_STATE)
?: LENS_STATE_STATIONARY
val afState = result.get(CaptureResult.CONTROL_AF_STATE)
?: CONTROL_AF_STATE_PASSIVE_FOCUSED
val aeState = result.get(CaptureResult.CONTROL_AE_STATE)
?: CONTROL_AE_STATE_CONVERGED
isFocused = lensState == LENS_STATE_STATIONARY
&& (afState == CONTROL_AF_STATE_INACTIVE || afState == CONTROL_AF_STATE_PASSIVE_FOCUSED)
&& aeState == CONTROL_AE_STATE_CONVERGED
// Log.d(TAG, "onCaptureCompleted: isFocused=$isFocused lensState=$lensState afState=$afState aeState=$aeState")
}
})
this.preview = previewBuilder
.build().also {
glPreviewRenderer.attachInputPreview(it)
}
// Select back camera
val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
val rotation = this.viewFinder.display.rotation
val capturedFrameData = ArrayList<CapturedImageData>()
val picturesDirectory = this.context.getExternalFilesDir(DIRECTORY_PICTURES)
val saveImageDirectory = File(picturesDirectory,
SimpleDateFormat(FILENAME_FORMAT,
Locale.US
).format(System.currentTimeMillis()))
var lastDecodedSendId = ""
var lastSentStepNumber = -1
var captureStartTime: Long = -1
var imageCounter = 0
// ImageAnalysis
imageAnalyzer = ImageAnalysis.Builder()
// .setTargetAspectRatio(screenAspectRatio)
.setTargetResolution(PREVIEW_TARGET_RESOLUTION)
// Set initial target rotation, we will have to call this again if rotation changes
// during the lifecycle of this use case
.setTargetRotation(rotation)
.build()
// The analyzer can then be assigned to the instance
.also {
resetDecodeProfiler()
var lastDecodeTime = System.currentTimeMillis()
it.setAnalyzer(cameraExecutor, CaptureImageAnalyzer(decoder, pfi) { result ->
result?.let { decodeResult ->
lastDecodeTime = System.currentTimeMillis()
if (!this.isCapturing) {
return@let
}
if (decodeResult.status == CaptureImageAnalyzerResultStatus.ProductNotFound) {
runOnUiThread {
this.cleanupResourcesOnPause()
this.shouldAutoResume = false
this.authListener.captureDetectedNoProductForDecodedValue(decodeResult.sendId)
this.authListener.setCaptureMessageText(null)
}
} else {
if (capturedFrameData.count() > 0
&& lastDecodedSendId != decodeResult.sendId) {
// product ID changed user probably started scanning a different barcode.
// restart the capture process.
resetDecodeProfiler()
capturedFrameData.clear()
this.fingerprintManager?.close()
this.fingerprintManager = null
captureStartTime = -1
}
if (lastDecodedSendId != decodeResult.sendId) {
// Only check for serial only when decoded value changes
// otherwise use the cached value.
val isSerialOnlyProduct = this.checkForSerialOnlyVerification(decodeResult.productIdentifier, decodeResult.sendId)
if (isSerialOnlyProduct && !isScanOnly) {
if (this.isCapturing) {
val productTitle = this.pfi.getTitle(decodeResult.productIdentifier)
val productDescription = this.pfi.getDescription(decodeResult.productIdentifier)
val productImage = this.pfi.getLocalImage(decodeResult.productIdentifier)
val image: Drawable? = if (productImage != null) {
val productImageCopy: Bitmap = productImage.copy(productImage.config, true)
BitmapDrawable(this.context.resources, productImageCopy)
} else {
null
}
runOnUiThread {
this.authListener.captureDetectedProductForDecodedValue(productTitle, productDescription, image, decodeResult.sendId)
this.authListener.showProgressBar()
this.cleanupResourcesOnPause()
this.shouldAutoResume = false
this.authListener.captureCompletedForSerialVerification(decodeResult.productIdentifier!!, decodeResult.sendId)
}
}
return@let
}
}
if (lastDecodedSendId != decodeResult.sendId) {
val productTitle = this.pfi.getTitle(decodeResult.productIdentifier)
val productDescription = this.pfi.getDescription(decodeResult.productIdentifier)
val productImage = this.pfi.getLocalImage(decodeResult.productIdentifier)
val image: Drawable? = if (productImage != null) {
val productImageCopy: Bitmap = productImage.copy(productImage.config, true)
BitmapDrawable(this.context.resources, productImageCopy)
} else {
null
}
runOnUiThread {
this.authListener.captureDetectedProductForDecodedValue(productTitle, productDescription, image, decodeResult.sendId)
}
}
lastDecodedSendId = decodeResult.sendId
runOnUiThread {
if (capturedFrameData.size >= REQ_FUSION_FRAME_COUNT) {
return@runOnUiThread
}
when (decodeResult.status) {
CaptureImageAnalyzerResultStatus.TooFar -> {
this.authListener.setCaptureMessageText(this.context.getString(R.string.closer))
}
CaptureImageAnalyzerResultStatus.TooClose -> {
this.authListener.setCaptureMessageText(this.context.getString(R.string.farther))
}
CaptureImageAnalyzerResultStatus.Rotate90 -> {
this.authListener.setCaptureMessageText(this.context.getString(R.string.rotate90))
}
CaptureImageAnalyzerResultStatus.ReadyToCapture -> {
if (this.stabilitySensorListener.isSteady) {
this.authListener.setCaptureMessageText(this.context.getString(R.string.capturing_hold_steady))
} else {
this.authListener.setCaptureMessageText(this.context.getString(R.string.capturing_motion_detected))
}
}
else -> {
TODO()
}
}
}
if (decodeResult.status == CaptureImageAnalyzerResultStatus.ReadyToCapture
&& this.stabilitySensorListener.isSteady) {
if (captureStartTime <= 0) {
captureStartTime = System.currentTimeMillis()
}
if (capturedFrameData.size < REQ_FUSION_FRAME_COUNT) {
this.takePicture(MAX_TAKE_PICTURE_QUEUE_LENGTH,
previewCornerPoints = decodeResult.cornerPoints,
takePictureStartingListener = {
if (this.isCapturing
&& capturedFrameData.size < REQ_FUSION_FRAME_COUNT) {
val currentStepNumber = capturedFrameData.size + 1
if (currentStepNumber != lastSentStepNumber) {
lastSentStepNumber = currentStepNumber
runOnUiThread {
this.authListener.onCaptureProgressStepStarting(currentStepNumber, REQ_FUSION_FRAME_COUNT)
}
}
}
}) { captureData, skippedCapture ->
if (!isCapturePictureBusy.compareAndSet(false, true)) {
// Log.d(TAG, "startCamera: Skipping takePicture due to lock $lastSentStepNumber")
captureData?.let { l_data ->
File(l_data.imagePath).delete()
}
return@takePicture
}
if (captureData == null) {
if (!skippedCapture) {
runOnUiThread {
this.authListener.onCaptureProgressStepCompleted(capturedFrameData.size + 1, REQ_FUSION_FRAME_COUNT, CaptureStepStatus.DecodeFailed, this.context.getString(R.string.capturing_hold_steady))
}
} else {
if (!isFocused) {
runOnUiThread {
this.authListener.setCaptureMessageText(this.context.getString(R.string.adjusting_camer))
}
}
}
// Log.d(TAG, "startCamera: Unlocking takePicture for step $lastSentStepNumber")
isCapturePictureBusy.set(false)
return@takePicture
}
if (!this.isCapturing) {
File(captureData.imagePath).delete()
// Log.d(TAG, "startCamera: Unlocking takePicture for step $lastSentStepNumber")
isCapturePictureBusy.set(false)
return@takePicture
}
if (!this.stabilitySensorListener.isSteady) {
File(captureData.imagePath).delete()
// Log.d(TAG, "startCamera: Unlocking takePicture for step $lastSentStepNumber (Camera not steady.)")
runOnUiThread {
this.authListener.setCaptureMessageText(this.context.getString(R.string.capturing_motion_detected))
}
isCapturePictureBusy.set(false)
return@takePicture
}
val engineProcessStartTime = System.currentTimeMillis()
if (this.fingerprintManager == null) {
this.fingerprintManager = FingerprintFusionManager(this.context,
BuildConfig.DEBUG || BuildConfig.CAN_DECRYPT,
this.pfi.getJSONForProductID(decodeResult.productIdentifier, decodeResult.sendId, serverUrl, userName, password))
}
val efpResult: FingerprintFusionManagerResult = if (capturedFrameData.size < REQ_FUSION_FRAME_COUNT - 1) {
this.fingerprintManager!!.addImage(imageFileName = captureData.imagePath,
cornerPoints = captureData.cornerPoints,
savePreview = false,
saveCropped = this.saveImages,
isRotated90 = decodeResult.needsUPCPrefix(),
calculateFocus = this.saveImages)
} else {
this.fingerprintManager!!.addImageAndGetFusedFP(imageFileName = captureData.imagePath,
cornerPoints = captureData.cornerPoints,
savePreview = true,
saveCropped = this.saveImages,
isRotated90 = decodeResult.needsUPCPrefix(),
calculateFocus = this.saveImages)
}
val engineProcessTime = System.currentTimeMillis() - engineProcessStartTime
Log.d(TAG, "startCamera: efpProcessingTime: ${engineProcessTime / 1000.0} seconds.")
if (capturedFrameData.size == REQ_FUSION_FRAME_COUNT) {
File(captureData.imagePath).delete()
efpResult.efpImagePath?.let { previewImagePath ->
File(previewImagePath).delete()
}
efpResult.scaledPreviewImagePath?.let { previewImagePath ->
File(previewImagePath).delete()
}
// Log.d(TAG, "startCamera: Unlocking takePicture for step $lastSentStepNumber")
isCapturePictureBusy.set(false)
return@takePicture
}
imageCounter += 1
if (efpResult.statusCode == 0) {
// Log.d(TAG, "startCamera: resultString->${efpResult.resultString}")
capturedFrameData.add(captureData)
if (saveImages) {
efpResult.efpImagePath?.let { efpImagePath ->
saveImageDirectory.mkdirs()
val fileNamePrefix = "${"%02d".format(imageCounter)}#${"%03d".format(efpResult.statusCode)}#${"%.4f".format(efpResult.measuredFocus)}#${"%.4f".format(engineProcessTime / 1000.0)}"
if (saveImageVerbose) {
val captureFile = File(captureData.imagePath)
val saveImageFile = File(saveImageDirectory, "$fileNamePrefix.${captureFile.extension}")
captureFile.copyTo(saveImageFile, true)
val captureDataFile = File(saveImageDirectory, "$fileNamePrefix-captureData.json")
val captureDataString = Gson().toJson(captureData)
captureDataFile.writeText(captureDataString)
val decodedDataFile = File(saveImageDirectory, "$fileNamePrefix-decodeData.json")
val decodedDataString = Gson().toJson(decodeResult)
decodedDataFile.writeText(decodedDataString)
}
val efpImageFile = File(efpImagePath)
val saveEfpImageFile = File(saveImageDirectory, "$fileNamePrefix-efp.${efpImageFile.extension}")
efpImageFile.copyTo(saveEfpImageFile, true)
}
}
runOnUiThread {
this.authListener.onCaptureProgressStepCompleted(capturedFrameData.size, REQ_FUSION_FRAME_COUNT, CaptureStepStatus.Success, this.context.getString(R.string.capturing_hold_steady))
}
if (capturedFrameData.size >= REQ_FUSION_FRAME_COUNT) {
runOnUiThread {
this.authListener.showProgressBar()
this.cleanupResourcesOnPause()
this.shouldAutoResume = false
}
val previewImagePath = efpResult.scaledPreviewImagePath!!
val imgBytes = File(previewImagePath).readBytes()
File(previewImagePath).delete()
File(captureData.imagePath).delete()
efpResult.efpImagePath?.let { imagePath ->
File(imagePath).delete()
}
val totalCaptureTime = (System.currentTimeMillis() - captureStartTime) / 1000.0
runOnUiThread {
val itemData = ItemCaptureData(displayValue = decodeResult.displayValue,
rawBytes = decodeResult.rawBytes,
sendId = decodeResult.sendId,
productIdentifier = decodeResult.productIdentifier,
barcodeFormat = decodeResult.barcodeFormat,
imgBytes = imgBytes,
fusedFP = efpResult.resultString,
captureTime = totalCaptureTime)
if (saveImages) {
val diagnosisItemData = ItemCaptureData(displayValue = decodeResult.displayValue,
rawBytes = decodeResult.rawBytes,
sendId = decodeResult.sendId,
productIdentifier = decodeResult.productIdentifier,
barcodeFormat = decodeResult.barcodeFormat,
imgBytes = "".toByteArray(),
fusedFP = efpResult.resultString,
captureTime = totalCaptureTime)
val itemDataFile = File(saveImageDirectory, "capture_item_data.json")
val itemDataString = Gson().toJson(diagnosisItemData)
itemDataFile.writeText(itemDataString)
}
this.authListener.captureCompletedSuccessfully(itemData, decodeResult.sendId)
}
} else {
File(captureData.imagePath).delete()
efpResult.efpImagePath?.let { previewImagePath ->
File(previewImagePath).delete()
}
efpResult.scaledPreviewImagePath?.let { previewImagePath ->
File(previewImagePath).delete()
}
}
} else {
Log.e(TAG, "startCamera: efp Error: ${efpResult.statusCode} calculatedFocus->${efpResult.measuredFocus} resultString->${efpResult.resultString}")
if (saveImages) {
saveImageDirectory.mkdirs()
val fileNamePrefix = "${"%02d".format(imageCounter)}#${"%03d".format(efpResult.statusCode)}#${"%.4f".format(efpResult.measuredFocus)}#${"%.4f".format(engineProcessTime / 1000.0)}"
if (saveImageVerbose) {
val captureFile = File(captureData.imagePath)
val saveImageFile = File(saveImageDirectory, "$fileNamePrefix.${captureFile.extension}")
captureFile.copyTo(saveImageFile, true)
val captureDataFile = File(saveImageDirectory, "$fileNamePrefix-captureData.json")
val captureDataString = Gson().toJson(captureData)
captureDataFile.writeText(captureDataString)
val decodedDataFile = File(saveImageDirectory, "$fileNamePrefix-decodeData.json")
val decodedDataString = Gson().toJson(decodeResult)
decodedDataFile.writeText(decodedDataString)
}
efpResult.efpImagePath?.let { efpImage ->
val efpImageFile = File(efpImage)
val saveEfpImageFile = File(saveImageDirectory, "$fileNamePrefix-efp.${efpImageFile.extension}")
efpImageFile.copyTo(saveEfpImageFile, true)
}
}
File(captureData.imagePath).delete()
efpResult.efpImagePath?.let { previewImagePath ->
File(previewImagePath).delete()
}
efpResult.scaledPreviewImagePath?.let { previewImagePath ->
File(previewImagePath).delete()
}
runOnUiThread {
val message = when (efpResult.statusCode) {
EFP_REJECT_INSUFFICIENT_FOCUS_CODE -> {
this.context.getString(R.string.efp_reject_insufficient_focus)
}
EFP_REJECT_INVALID_BRIGHTNESS_CODE -> {
this.context.getString(R.string.efp_reject_unacceptable_brightness)
}
else -> {
this.context.getString(R.string.capturing_hold_steady)
}
}
this.authListener.onCaptureProgressStepCompleted(capturedFrameData.size + 1, REQ_FUSION_FRAME_COUNT, CaptureStepStatus.EngineRejected, message)
}
}
// Log.d(TAG, "startCamera: Unlocking takePicture for step $lastSentStepNumber")
isCapturePictureBusy.set(false)
}
}
}
}
} ?: run {
val timeSinceLastDecode = System.currentTimeMillis() - lastDecodeTime
if (timeSinceLastDecode > NO_BARCODE_MAX_DECODE_TIME) {
// If no decode for 4 seconds reset the timer
// And raise a looking for barcode message.
if (this.isCapturing
&& capturedFrameData.size < REQ_FUSION_FRAME_COUNT) {
val currentStepNumber = capturedFrameData.size + 1
lastDecodeTime = System.currentTimeMillis()
runOnUiThread {
this.authListener.setCaptureMessageText(this.context.getString(R.string.looking_for_barcode))
}
if (currentStepNumber > 1
&& currentStepNumber != lastSentStepNumber) {
lastSentStepNumber = currentStepNumber
runOnUiThread {
this.authListener.onCaptureProgressStepStarting(currentStepNumber, REQ_FUSION_FRAME_COUNT)
}
}
}
}
}
})
}
// ImageCapture
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
// We request aspect ratio but no resolution to match preview config, but letting
// CameraX optimize for whatever specific resolution best fits our use cases
.setTargetAspectRatio(screenAspectRatio)
// Set initial target rotation, we will have to call this again if rotation changes
// during the lifecycle of this use case
.setTargetRotation(rotation)
.build()
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
deviceCalibration?.let { calibration ->
currentAnalysisDPI = calibration.dpi
}
// Bind use cases to camera
camera = cameraProvider.bindToLifecycle(
(this.context as AppCompatActivity), cameraSelector, preview, imageAnalyzer, imageCapture)
// val zoomInfo = camera?.cameraInfo?.zoomState?.value
// zoomInfo?.let {
// Log.d(TAG, "startCamera: zoomInfo ${it.linearZoom} ${it.minZoomRatio} ${it.maxZoomRatio} ${it.zoomRatio}")
// }
this.isCapturing = true
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
this.isCameraStarting = false
}, ContextCompat.getMainExecutor((this.context as AppCompatActivity)))
}
private fun checkForSerialOnlyVerification(productId: String?, decodedValue: String): Boolean {
if (productId != null && preferences.isSerialNumberVerificationSupported &&
pfi.getSerialOnlyVerification(productId)) {
return true
}
return false
}
private fun takePicture(maxQueueLength: Int, previewCornerPoints: Array<Point>,
takePictureStartingListener:(() -> Unit)?,
listener: ((imageData: CapturedImageData?, skipped:Boolean) -> Unit)?) {
if (!this.isCapturing) {
return
}
imageCapture?.let { imageCapture ->
val outputDirectory = this.context.cacheDir
val photoFile = File(
outputDirectory,
SimpleDateFormat(FILENAME_FORMAT, Locale.US
).format(System.currentTimeMillis()) + ".jpg")
// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
// Setup image capture listener which is triggered after photo has
// been taken
takePictureStartingListener?.invoke()
if (!this.isCapturing || !isFocused) {
listener?.invoke(null, true)
}
val counter = takePictureAtomicCounter.incrementAndGet()
if (counter > maxQueueLength) {
listener?.invoke(null, true)
takePictureAtomicCounter.decrementAndGet()
return
}
val captureStartTime = System.currentTimeMillis()
imageCapture.takePicture(
outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
val totalCaptureTime = (System.currentTimeMillis() - captureStartTime)
Log.e(TAG, "onError: ImageCapture (Time: ${totalCaptureTime/1000.0} seconds) failed. Error -> ${exc.message}", exc)
photoFile.delete()
listener?.invoke(null, false)
takePictureAtomicCounter.decrementAndGet()
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val totalCaptureTime = (System.currentTimeMillis() - captureStartTime)
// Log.d(TAG, "onImageSaved: ImageCapture counter->${takePictureAtomicCounter.get()} (Time: ${totalCaptureTime/1000.0} seconds)")
takePictureAtomicCounter.decrementAndGet()
val capturedBitmap = BitmapFactory.decodeFile(photoFile.absolutePath)
// Log.d(TAG, "onCaptureSuccess: captureBitmap ${capturedBitmap.width} x ${capturedBitmap.height}")
try {
val xScale = capturedBitmap.width.toFloat() / previewSize.width
val yScale = capturedBitmap.height.toFloat() / previewSize.height
// Note: xScale and yScale should be equal. However on some devices
// like the Oppo, the captured image is rotated but the preview is not.
// So if xScale and yScale do not match assume this scenario and
// calculate the scale based on highResHeight/previewWidth
val scale = if (xScale == yScale) {
xScale
} else {
capturedBitmap.height.toFloat() / previewSize.width
}
val scaledPoints = previewCornerPoints.map { point ->
PointF((point.x * scale), (point.y * scale))
}.toTypedArray()
listener?.invoke(CapturedImageData(imagePath = photoFile.absolutePath, cornerPoints = scaledPoints, totalCaptureTime = totalCaptureTime), false)
} finally {
capturedBitmap.recycle()
}
}
})
}
}
private fun sendAuthenticationRequest(request: Request, authPkId: String, captureTime: Double) {
val authProcessStartTime = System.currentTimeMillis()
val builder = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
val client = builder.build()
try {
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
parseAuthResponse(
response,
authProcessStartTime,
authPkId,
captureTime)
}
override fun onFailure(call: Call, e: IOException) {
// Log.e(TAG, "authenticateItem onFailure:", e)
runOnUiThread {
authListener.showAuthenticationErrorMessage("Connection Error", "There was a problem connecting to the server. Please verify you have network connection.")
}
}
})
} catch (e: IOException) {
runOnUiThread {
authListener.showAuthenticationErrorMessage("Connection Error", "There was a problem connecting to the server. Please verify you have network connection.")
}
}
}
private fun parseAuthResponse(response: Response,
authProcessStartTime: Long,
authPkId: String,
captureTime: Double) {
val responseBody = response.body?.string()
val msgBody = getMsgBody(this@AuthView.context, responseBody, response.code)
//Log.d(TAG, "authenticateItem: $responseBody")
if (response.isSuccessful && responseBody != null) {
val totalAuthTime = (System.currentTimeMillis() - authProcessStartTime)/1000.0
val authenticationResponse = AuthenticationResponse.parse(responseBody, totalAuthTime, captureTime)
authenticationResponse.pkid = authPkId
runOnUiThread {
authListener.authenticationCompletedWithResult(authenticationResponse)
authListener.hideProgressBar()
}
} else {
runOnUiThread {
if(response.code == AUTHORIZATION_ERROR_CODE) {
authListener.showErrorMessageWithPasswordReset("Authentication Error", msgBody)
} else {
authListener.showAuthenticationErrorMessage("Authentication Error", msgBody)
}
}
}
}
private fun cleanupResourcesOnPause() {
this.sensorManager.unregisterListener(this.stabilitySensorListener)
(this.context as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
this.fingerprintManager?.close()
this.fingerprintManager = null
this.activeCameraProvider?.let {
this.isCapturing = false
it.unbindAll()
this.activeCameraProvider = null
}
}
private fun getCalibratedAverageValueAtDPI(dpi: Int, analysisData: MutableMap<Int, MutableList<Float>>):Float {
analysisData[dpi]?.let {
it.let { focusScoresList ->
return focusScoresList.toList().average().toFloat()
}
}
return 0.0f
}
private fun discardCalibratedUnRelatedValuesAtDPI(dpi: Int, analysisData: MutableMap<Int, MutableList<Float>>) {
analysisData[dpi]?.let {
val sortedFocusScores = it.toList().sorted()
val trimmedAverage = sortedFocusScores.subList(0, sortedFocusScores.size - 2).average()
for (i in it.size-1 downTo 0) {
val diff = it[i] - trimmedAverage
if (diff > 0.3) {
// Log.d(TAG, "discardCalibratedUnRelatedValuesAtDPI: removing unrelated focusScore->${it[i]} trimmedAvg->$trimmedAverage")
it.removeAt(i)
}
}
}
}
private fun getCalibratedDPIValue(analysisData: MutableMap<Int, MutableList<Float>>):Pair<Int, Float> {
var bestDPI = -1
var bestScore = 10000.0f
analysisData.keys.forEach { dpi ->
val medianScore = getCalibratedAverageValueAtDPI(dpi, analysisData)
if (medianScore != 0.0f
&& medianScore < bestScore) {
bestDPI = dpi
bestScore = medianScore
}
}
return Pair(bestDPI, bestScore)
}
private fun uploadDeviceCalibration(calibrationResult: DeviceCalibrationResult) {
saveLocalCalibration(context, calibrationResult)
val listener = resultListener<Any>(
onSuccess = {
try {
// Log.d(TAG, "syncDeviceCalibration-PUT: $it")
} catch (e: Exception) {
// Log.e(TAG, "syncDeviceCalibration-PUT: error->", e)
}
},
onComplete = {
// Log.d(TAG, "syncDeviceCalibration-PUT: Complete for com.systechone.calibration.${Build.MODEL}")
})
dataSetupManager.putAppPreference("com.systechone.calibration.${Build.MODEL}", Gson().toJson(calibrationResult), listener)
}
companion object {
private const val TAG = "SECSL_V2.AuthView"
private const val DEFAULT_REQ_DPI_AT_1080P = 350
private const val CALIBRATION_MIN_DPI_AT_1080P = 300
private const val MM_PER_INCH = 25.4
private const val REQ_FUSION_FRAME_COUNT = 6
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val CALIBRATION_FRAME_COUNT = 6
private const val CALIBRATION_MAX_TAKE_PICTURE_QUEUE_LENGTH = 2
private const val MAX_TAKE_PICTURE_QUEUE_LENGTH = 4
private val PREVIEW_TARGET_RESOLUTION = Size(1440, 1920)
private const val EFP_REJECT_INSUFFICIENT_FOCUS_CODE = 509
private const val EFP_REJECT_INVALID_BRIGHTNESS_CODE = 512
private const val NO_BARCODE_MAX_DECODE_TIME = 4000
private var previewSize = Size(0, 0)
private var isFocused = false
private var currentAnalysisDPI: Int = DEFAULT_REQ_DPI_AT_1080P
private const val CALIBRATION_FILE_NAME = "com.systech.calibration.devicecalibration.json"
private var decodeFrameCount = AtomicInteger(0)
private var totalDecodeFrameTime = AtomicLong(0)
internal fun clearCachedCalibration(context: Context) {
val calibrationFile = File(context.cacheDir, CALIBRATION_FILE_NAME)
calibrationFile.delete()
}
/**
* @hide
* @suppress
*/
fun getCachedDeviceCalibration(context: Context) : DeviceCalibrationResult? {
val calibrationFile = File(context.cacheDir, CALIBRATION_FILE_NAME)
if (calibrationFile.exists()) {
// try to parse and load existing cached device calibration
val calibrationContents = calibrationFile.readAsString()
val calibration = Gson().fromJson(calibrationContents, DeviceCalibrationResult::class.java)
calibration?.let {
if (it.version == DeviceCalibrationResult.EXPECTED_CALIBRATION_VERSION) {
return it
}
}
}
return null
}
/**
* Gets the device calibration if available. If a cached verison is not available from an earlier
* cloud connection request, a cloud connection request is made to retrieve the device calibration.
* If none is available null will be returned.
*/
suspend fun getDeviceCalibration(context: Context, dataSetupManager: SECSL_V1_DataSetupManager): DeviceCalibrationResult? {
val calibrationFile = File(context.cacheDir, CALIBRATION_FILE_NAME)
if (calibrationFile.exists()) {
// try to parse and load existing cached device calibration
val calibrationContents = calibrationFile.readAsString()
val calibration = Gson().fromJson(calibrationContents, DeviceCalibrationResult::class.java)
calibration?.let {
if (it.version == DeviceCalibrationResult.EXPECTED_CALIBRATION_VERSION) {
return it
}
}
}
var calibration:DeviceCalibrationResult? = DeviceCalibrationPresets.getPresetCalibration()
calibration?.let {
if (it.version == DeviceCalibrationResult.EXPECTED_CALIBRATION_VERSION) {
return it
}
}
return suspendCoroutine { continuation ->
val listener = resultListener<Any?>(
onSuccess = {
try {
// Log.d(TAG, "getDeviceCalibration: $it")
val rootTreeMap = it as? LinkedTreeMap<*, *>
rootTreeMap?.let { treeMap ->
val calibrationString = treeMap["value"] as? String
calibrationString?.let { jsonString ->
calibration = Gson().fromJson(jsonString, DeviceCalibrationResult::class.java)
}
}
} catch (e: Exception) {
// Log.e(TAG, "getDeviceCalibration: error->", e)
}
},
onComplete = {
// Log.d(TAG, "getDeviceCalibration: Complete for com.systechone.calibration.${Build.MODEL}")
calibration?.let { downloadedCalibration ->
if (downloadedCalibration.version == DeviceCalibrationResult.EXPECTED_CALIBRATION_VERSION) {
saveLocalCalibration(context, downloadedCalibration)
continuation.resume(calibration)
}
else {
continuation.resume(null)
}
} ?: run {
continuation.resume(null)
}
})
dataSetupManager.getAppPreference("com.systechone.calibration.${Build.MODEL}", listener)
}
}
internal fun saveLocalCalibration(context: Context, calibration: DeviceCalibrationResult) {
val calibrationFile = File(context.cacheDir, CALIBRATION_FILE_NAME)
val calibrationString = Gson().toJson(calibration)
calibrationFile.writeText(calibrationString)
}
private fun ImageProxy.toBitmap(): Bitmap {
val yBuffer = planes[0].buffer // Y
val uBuffer = planes[1].buffer // U
val vBuffer = planes[2].buffer // V
val ySize = yBuffer.remaining()
val uSize = uBuffer.remaining()
val vSize = vBuffer.remaining()
val nv21 = ByteArray(ySize + uSize + vSize)
//U and V are swapped
yBuffer.get(nv21, 0, ySize)
vBuffer.get(nv21, ySize, vSize)
uBuffer.get(nv21, ySize + vSize, uSize)
val yuvImage = YuvImage(nv21, NV21, this.width, this.height, null)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
val imageBytes = out.toByteArray()
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
private fun Barcode.toUniSecureSendID(): String {
val rawString = if (this.format == Barcode.CODE_128 && this.rawValue.startsWith("]C1")) {
// If the barcode is a GS1 encoded code 128, the decoder does not include the binary markers.
// Instead it provided a string starting with "]C1". The cloud expects this to be removed
// along with all other non printable characters. So we remove it here.
this.rawValue.substring(3)
} else {
this.rawValue
}
return rawString.filter { it.toByte().toInt() in 32..126 }
}
private fun String.isGS1Encoded(): Boolean {
if (this.length >= 17) {
if (this[0].toByte().toInt() == 29
&& this[1].toByte().toInt() == 48
&& this[2].toByte().toInt() == 49) {
return true
}
}
return false
}
private fun String.getGTIN(): String? {
if (this.isGS1Encoded()) {
return this.substring(3, 17)
}
return null
}
private fun Rect.inferredBoundingBoxForAlternateDecoder(codeSize: Size, dpi: Int, maxWidth: Int): Rect {
val expectedHeightInMM = codeSize.height * dpi / MM_PER_INCH
return Rect(max(0, this.centerX() - (expectedHeightInMM/2).toInt()),
this.top,
min(this.centerX() + (expectedHeightInMM/2).toInt(), maxWidth),
this.bottom)
}
private fun buildInferredCornerPointsForAlternateDecoder(cornerPoints: Array<Point>,
codeSize: Size,
dpi: Int,
maxHeight: Int): Array<Point> {
val expectedHeightInPixels = codeSize.height * dpi / MM_PER_INCH
val centerY = (maxHeight) / 2
val top = max(0, centerY - (expectedHeightInPixels / 2).toInt())
val bottom = min(centerY + (expectedHeightInPixels / 2).toInt(), maxHeight)
val topLeft = Point(top, cornerPoints[0].y)
val topRight = Point(bottom, cornerPoints[1].y)
val bottomRight = Point(bottom, cornerPoints[2].y)
val bottomLeft = Point(top, cornerPoints[3].y)
return arrayOf(topLeft, topRight, bottomRight, bottomLeft)
}
private fun getPreviewDPIForCodeSize(codeSize: Size, analysisDPI: Int): Int {
return min(previewSize.width, previewSize.height) * analysisDPI / 1080
}
private fun resetDecodeProfiler() {
decodeFrameCount.set(0)
totalDecodeFrameTime.set(0)
}
private fun getMsgBody(context: Context, response: String?, code: Int): String {
val msgBody = response ?: "Unknown Error during authentication."
return if(code == AUTHORIZATION_ERROR_CODE) context.getString(R.string.invalid_username_pass) else msgBody
}
private fun findProductMatchForSearchString(searchString: String, allWidths:String): String? {
val productsString = allWidths.split('~')
for (i in 0 until productsString.count()) {
val productString = productsString[i]
if (productString.isEmpty()) {
continue
}
val productDetails = productString.split('`')
if (productDetails.isNullOrEmpty()) {
continue
}
if (productDetails.count() <= 4) {
continue
}
val searchFor = productDetails[0]
val strStartIndex = productDetails[2]
val strSearchLength = productDetails[3]
val fullMatchOnly = !(strStartIndex.toIntOrNull() != null && strSearchLength.toIntOrNull() != null)
val startIndex = (strStartIndex.toIntOrNull() ?: 1) - 1
val searchLength = strSearchLength.toIntOrNull() ?: searchFor.length
if (searchString.length >= startIndex+searchLength) {
if(fullMatchOnly
&& searchString.length != searchFor.length) {
continue;
}
val splitSearchString = searchString.substring(startIndex, startIndex + searchLength)
if (splitSearchString.compareTo(searchFor, true) == 0) {
return searchFor
}
}
}
return null
}
private fun getProductIdFromDecodedData(rawBarcodeString: String,
unisecureSendId: String,
pfi: ProductFamilyInfo) : String? {
if (rawBarcodeString.isGS1Encoded()) {
val searchString = rawBarcodeString.getGTIN()!!
val searchFor = findProductMatchForSearchString(searchString, pfi.allCodeWidths)
searchFor?.let {
return it
}
}
return findProductMatchForSearchString(unisecureSendId, pfi.allCodeWidths)
}
internal fun performFPFusionAndAuthentication(
context: Context,
captureData: ItemCaptureData,
imageFiles: List<File>,
serverUrl: String,
userName: String,
password: String,
pfi: ProductFamilyInfo,
preferences: Preferences,
location: Location?,
callback: performFPFusionAndAuthenticationCallback
) {
GlobalScope.launch {
val rawBarcodeString = String(captureData.rawBytes, Charsets.UTF_8)
val productId = getProductIdFromDecodedData(rawBarcodeString,
captureData.sendId,
pfi)
if (productId == null) {
runOnUiThread {
callback(null, "Product Not Found")
}
return@launch
}
val efpConfig = pfi.getJSONForProductID(productId,
captureData.sendId, serverUrl, userName, password)
if (efpConfig == "0") {
runOnUiThread {
callback(null, "Product Not Found")
}
return@launch
}
val localFPManager = FingerprintFusionManager(context,
BuildConfig.DEBUG || BuildConfig.CAN_DECRYPT,
efpConfig
)
localFPManager.use {
val dummyCornerPoints = arrayOf<PointF>()
var fusedFingerprint: String? = null
var imgBytes: ByteArray? = null
for (i in imageFiles.indices) {
val imageFile = imageFiles[i]
val result = if (i < imageFiles.size - 1) {
localFPManager.addImage(imageFile.absolutePath,
dummyCornerPoints, savePreview = false, saveCropped = false, isRotated90 = false)
} else {
localFPManager.addImageAndGetFusedFP(imageFile.absolutePath,
dummyCornerPoints, savePreview = true, saveCropped = false, isRotated90 = false)
}
if (result.statusCode != 0) {
runOnUiThread {
callback(null, "efpError: ${result.statusCode}")
}
return@use
}
if (i == imageFiles.size - 1) {
val previewImagePath = result.scaledPreviewImagePath!!
imgBytes = File(previewImagePath).readBytes()
File(previewImagePath).delete()
fusedFingerprint = result.resultString
}
}
fusedFingerprint?.let { fingerprintToAuthenticate ->
pfi.setValuesForCode(productId)
val isSerialized = pfi.getSerialized(productId)
val profileId = pfi.getProfileID(productId)
val productItemId = pfi.getCurrentItemID(productId)
val authPkId = pfi.getPkid(productId)
val bcLabel: String = if (captureData.needsUPCPrefix()) {
"UPC${captureData.sendId}"
} else {
captureData.sendId
}
val wPath = if (isSerialized) {
"/fingerprint/api/verification/?format=json"
} else {
"/fingerprint/api/non-serial-verification/?format=json"
}
val uploadRequest = UniSecureSDK.createAuthenticateFPRequest(
fingerprintToAuthenticate,
imgBytes!!,
bcLabel,
location?.latitude ?:0.0,
location?.longitude?:0.0,
profileId,
productItemId,
serverUrl,
userName,
password,
wPath,
preferences.deviceRegistrationID,
context.applicationContext)
val authProcessStartTime = System.currentTimeMillis()
val builder = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
val client = builder.build()
try {
client.newCall(uploadRequest).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val responseBody = response.body?.string()
if (response.isSuccessful) {
val totalAuthTime = (System.currentTimeMillis() - authProcessStartTime) / 1000.0
val authenticationResponse = AuthenticationResponse.parse(responseBody, totalAuthTime, captureData.captureTime)
authenticationResponse.pkid = authPkId
runOnUiThread {
callback(authenticationResponse, null)
}
} else {
runOnUiThread {
callback(null, "Authentication Error:${getMsgBody(context, responseBody, response.code)}")
}
}
}
override fun onFailure(call: Call, e: IOException) {
runOnUiThread {
callback(null, "Connection Error: There was a problem connecting to the server. Please verify you have network connection.")
}
}
})
} catch (e: IOException) {
runOnUiThread {
callback(null, "Connection Error: There was a problem connecting to the server. Please verify you have network connection.")
}
}
}
}
}
}
}
private open class CaptureImageAnalyzer(
private val decoder: BarcodeDecoderV2,
private val pfi: ProductFamilyInfo,
private val listener: FrameAnalysisListener?
) : ImageAnalysis.Analyzer {
override fun analyze(image: ImageProxy) {
val startTime = System.currentTimeMillis()
previewSize = Size(image.width, image.height)
// Log.d(TAG, "analyze: image.width:${image.width} height: ${image.height} rotation: ${image.imageInfo.rotationDegrees}")
val rotationDegrees = image.imageInfo.rotationDegrees
val bitmap = BitmapUtils.getBitmap(image)
image.close()
try {
// Log.d(TAG, "analyze: analyzeBitmap ${bitmap.width} x ${bitmap.height}")
val barcode = decoder.decodeBarcode(bitmap, rotationDegrees)
val totalTime = totalDecodeFrameTime.addAndGet(System.currentTimeMillis() - startTime).absoluteValue
val decoderFrameCount = decodeFrameCount.incrementAndGet().absoluteValue
// Log.d(TAG, "analyze: TotalDecoderTime->$totalTime TotalFrames->$decoderFrameCount Avg: ${"%.4f".format(totalTime/(decoderFrameCount * 1000.0))}")
barcode?.also {
// Log.d(TAG, "analyze: timeStamp: ${image.imageInfo.timestamp} barcodeValue: ${it.displayValue}")
lookupProduct(it)?.let { productId ->
pfi.setValuesForCode(productId)
val codeSize = pfi.getCodeSizeInMM(productId)
// Calculating the width in mm at 32px per mm at 1080 width.
// @32px per mm for 1080 the DPI would be 813.
// This is what we use for TooBig, TooSmall calculations
// in UniSecureSDKCommon.cpp.
// However most UPCs wont fit this requirements and we would
// start backing off the required DPI. For UPCs we would require
// a max width of 690. Meaning the effective DPI would be much lower.
// a 35mm wide UPC was only giving 13px per mm effective DPI.
// See https://pixelcalculator.com/index.php
val analysisDPI = currentAnalysisDPI
val is1D = (barcode.format != DATA_MATRIX && barcode.format != QR_CODE)
val previewDPI = getPreviewDPIForCodeSize(codeSize, analysisDPI)
val boundingBox = if (is1D) {
it.boundingBox.inferredBoundingBoxForAlternateDecoder(codeSize, previewDPI, bitmap.width)
}
else {
it.boundingBox
}
val cornerPoints = if (is1D) {
buildInferredCornerPointsForAlternateDecoder(it.cornerPoints, codeSize, previewDPI, bitmap.height)
}
else {
it.cornerPoints
}
val widthInMM = if (is1D) {
boundingBox.height() * MM_PER_INCH / previewDPI
} else {
boundingBox.width() * MM_PER_INCH / previewDPI
}
val heightInMM = if (is1D) {
boundingBox.width() * MM_PER_INCH / previewDPI
} else {
boundingBox.height() * MM_PER_INCH / previewDPI
}
// Log.d(TAG, "analyze: productID $productId Expected dimensions: ${codeSize.width}mm x ${codeSize.height}mm")
var status = CaptureImageAnalyzerResultStatus.ReadyToCapture
val heightToWidthRatio = if(is1D) {
it.boundingBox.height().toDouble() / it.boundingBox.width().toDouble()
} else {
1.0
}
if (heightToWidthRatio < 1.0) {
status = CaptureImageAnalyzerResultStatus.Rotate90
}
else {
val farAdjustmentFactor = if(is1D) {
1.1
}
else {
1.0
}
val closeAdjustmentFactor = if(codeSize.width <= 6 || codeSize.height <= 12) 1.1 else 1.05
if ((widthInMM.toFloat() * farAdjustmentFactor) < codeSize.width) {
// Since 1D codes don't provide reliable height scanning, just check widths.
status = CaptureImageAnalyzerResultStatus.TooFar
}
else {
if (widthInMM > codeSize.width.toFloat() * closeAdjustmentFactor) {
status = CaptureImageAnalyzerResultStatus.TooClose
}
}
}
val result = CaptureImageAnalyzerResult(
status = status,
displayValue = it.displayValue,
rawBytes = it.rawValue.toByteArray(),
sendId = it.toUniSecureSendID(),
productIdentifier = productId,
cornerPoints = cornerPoints,
boundingBox = boundingBox,
decodedSizeInMM = SizeF(widthInMM.toFloat(), heightInMM.toFloat()),
expectedCodeSizeInMM = codeSize,
barcodeFormat = it.format,
analysisDPI = analysisDPI)
listener?.invoke(result)
} ?: run {
val analysisDPI = currentAnalysisDPI
val previewDPI = min(previewSize.width, previewSize.height) * analysisDPI / 1080
val is1D = (barcode.format != DATA_MATRIX && barcode.format != QR_CODE)
val widthInMM = if (is1D) {
it.boundingBox.height() * MM_PER_INCH / previewDPI
} else {
it.boundingBox.width() * MM_PER_INCH / previewDPI
}
val heightInMM = if (is1D) {
it.boundingBox.width() * MM_PER_INCH / previewDPI
} else {
it.boundingBox.height() * MM_PER_INCH / previewDPI
}
val result = CaptureImageAnalyzerResult(
status = CaptureImageAnalyzerResultStatus.ProductNotFound,
displayValue = it.displayValue,
rawBytes = it.rawValue.toByteArray(),
sendId = it.toUniSecureSendID(),
productIdentifier = null,
cornerPoints = it.cornerPoints,
boundingBox = it.boundingBox,
decodedSizeInMM = SizeF(widthInMM.toFloat(), heightInMM.toFloat()),
expectedCodeSizeInMM = Size(0, 0),
barcodeFormat = it.format,
analysisDPI = currentAnalysisDPI)
listener?.invoke(result)
}
} ?: run {
listener?.invoke(null)
}
} finally {
bitmap.recycle()
}
}
protected fun lookupProduct(decodedBarcode: Barcode): String? {
return getProductIdFromDecodedData(decodedBarcode.rawValue,
decodedBarcode.toUniSecureSendID(), pfi)
}
}
}
private enum class CaptureImageAnalyzerResultStatus {
ProductNotFound,
TooFar,
TooClose,
ReadyToCapture,
Rotate90
}
private data class CaptureImageAnalyzerResult(
val status: CaptureImageAnalyzerResultStatus,
val displayValue: String,
val rawBytes: ByteArray,
val sendId: String,
val productIdentifier: String?,
val cornerPoints: Array<Point>,
val boundingBox: Rect,
val decodedSizeInMM: SizeF,
val expectedCodeSizeInMM: Size,
val barcodeFormat: Int,
val analysisDPI: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CaptureImageAnalyzerResult
if (status != other.status) return false
if (displayValue != other.displayValue) return false
if (!rawBytes.contentEquals(other.rawBytes)) return false
if (sendId != other.sendId) return false
if (productIdentifier != other.productIdentifier) return false
if (!cornerPoints.contentEquals(other.cornerPoints)) return false
if (boundingBox != other.boundingBox) return false
if (decodedSizeInMM != other.decodedSizeInMM) return false
if (expectedCodeSizeInMM != other.expectedCodeSizeInMM) return false
if (barcodeFormat != other.barcodeFormat) return false
if (analysisDPI != other.analysisDPI) return false
return true
}
override fun hashCode(): Int {
var result = status.hashCode()
result = 31 * result + displayValue.hashCode()
result = 31 * result + rawBytes.contentHashCode()
result = 31 * result + sendId.hashCode()
result = 31 * result + (productIdentifier?.hashCode() ?: 0)
result = 31 * result + cornerPoints.contentHashCode()
result = 31 * result + boundingBox.hashCode()
result = 31 * result + decodedSizeInMM.hashCode()
result = 31 * result + expectedCodeSizeInMM.hashCode()
result = 31 * result + barcodeFormat
result = 31 * result + analysisDPI
return result
}
fun needsUPCPrefix():Boolean {
return !(
this.barcodeFormat == QR_CODE
|| this.barcodeFormat == DATA_MATRIX
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment