package com.taf.attendance import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import androidx.core.net.toFile import com.google.mlkit.vision.common.InputImage import com.google.mlkit.vision.face.FaceDetection import com.google.mlkit.vision.face.FaceDetectorOptions import org.tensorflow.lite.Interpreter import java.nio.ByteBuffer import java.nio.ByteOrder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File class FaceAuth(private val context: Context) { private val prefs = context.getSharedPreferences("faceauth", Context.MODE_PRIVATE) private val detectorOptions = FaceDetectorOptions.Builder() .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST) .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL) .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL) .build() private val faceDetector = FaceDetection.getClient(detectorOptions) private val interpreter: Interpreter by lazy { Interpreter(loadModelFile("mobile_facenet.tflite")) } private fun loadModelFile(modelName: String): ByteBuffer { val fd = context.assets.openFd(modelName) val input = java.io.FileInputStream(fd.fileDescriptor) val buffer = input.channel.map(java.nio.channels.FileChannel.MapMode.READ_ONLY, fd.startOffset, fd.declaredLength) input.close() fd.close() return buffer } private fun FloatArray.toPrefString(): String = joinToString(",") private fun String.toFloatArray(): FloatArray = if (isEmpty()) FloatArray(0) else split(",").map { it.toFloat() }.toFloatArray() private suspend fun getEmbedding(bitmap: Bitmap): FloatArray? = withContext(Dispatchers.Default) { val image = InputImage.fromBitmap(bitmap, 0) val faces = faceDetector.process(image).awaitResult() ?: return@withContext null if (faces.isEmpty()) return@withContext null val box = faces[0].boundingBox val faceBmp = Bitmap.createBitmap( bitmap, box.left.coerceAtLeast(0), box.top.coerceAtLeast(0), box.width().coerceAtMost(bitmap.width - box.left), box.height().coerceAtMost(bitmap.height - box.top) ) val inputBmp = Bitmap.createScaledBitmap(faceBmp, 112, 112, true) val buffer = ByteBuffer.allocateDirect(1 * 112 * 112 * 3 * 4).order(ByteOrder.nativeOrder()) for (y in 0 until 112) { for (x in 0 until 112) { val pixel = inputBmp.getPixel(x, y) val r = ((pixel shr 16 and 0xFF) - 127.5f) / 128f val g = ((pixel shr 8 and 0xFF) - 127.5f) / 128f val b = ((pixel and 0xFF) - 127.5f) / 128f buffer.putFloat(r) buffer.putFloat(g) buffer.putFloat(b) } } val outputDim = interpreter.getOutputTensor(0).shape()[1] val output = Array(1) { FloatArray(outputDim) } buffer.rewind() interpreter.run(buffer, output) output[0] } fun isFaceRegistered(): Boolean = prefs.contains("face_embed") suspend fun saveFace(path: String): Boolean = withContext(Dispatchers.Default) { try { val bmp = BitmapFactory.decodeFile(path) val embed = getEmbedding(bmp) ?: return@withContext false prefs.edit() .putString("face_path", path) .putString("face_embed", embed.toPrefString()) .apply() true } catch (e: Exception) { false } } fun getRegisteredFacePath(): String? = prefs.getString("face_path", null) private fun getRegisteredEmbedding(): FloatArray? = prefs.getString("face_embed", null)?.toFloatArray() /** * Verify that the provided photo matches the registered face. * * The photo is processed with ML Kit to extract the face region and then * passed through the bundled MobileFaceNet model. A cosine similarity check * compares the resulting embedding with the stored reference. */ suspend fun verifyFace(photoPath: String): Boolean = withContext(Dispatchers.Default) { val registered = getRegisteredEmbedding() ?: return@withContext false return@withContext try { val bmp = BitmapFactory.decodeFile(photoPath) val embed = getEmbedding(bmp) ?: return@withContext false val score = cosineSimilarity(registered, embed) score > 0.8f } catch (e: Exception) { false } } /** * Perform a lightweight liveness check. * * The first frame is analysed with ML Kit and the result is considered * valid when a single face is present and both eyes are open with a * probability over 0.5. This is not as robust as a full blink detection but * prevents obvious spoofing with a static photo. */ suspend fun runLivenessCheck(frames: List): Boolean = withContext(Dispatchers.Default) { if (frames.isEmpty()) return@withContext true val image = InputImage.fromBitmap(frames[0], 0) val faces = faceDetector.process(image).awaitResult() ?: return@withContext false if (faces.isEmpty()) return@withContext false val face = faces[0] val eyesOpen = (face.leftEyeOpenProbability ?: 1f) > 0.5f && (face.rightEyeOpenProbability ?: 1f) > 0.5f eyesOpen } private fun cosineSimilarity(v1: FloatArray, v2: FloatArray): Float { if (v1.size != v2.size) return 0f var dot = 0f var mag1 = 0f var mag2 = 0f for (i in v1.indices) { dot += v1[i] * v2[i] mag1 += v1[i] * v1[i] mag2 += v2[i] * v2[i] } mag1 = kotlin.math.sqrt(mag1) mag2 = kotlin.math.sqrt(mag2) return dot / (mag1 * mag2) } }