Created
January 27, 2021 07:24
-
-
Save RobertApikyan/3ca9afc1fb2b778432f1cdd1b4fef299 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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