package com.taf.attendance import android.app.Activity import android.content.Intent import android.net.Uri import androidx.core.net.toFile import android.os.Bundle import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.lifecycle.lifecycleScope import com.taf.attendance.data.AttendanceRepository import android.graphics.BitmapFactory import android.content.pm.PackageManager import android.widget.Toast import kotlinx.coroutines.launch import java.io.File class MainActivity : AppCompatActivity() { private lateinit var repo: AttendanceRepository private lateinit var faceAuth: FaceAuth private lateinit var cameraLauncher: ActivityResultLauncher private lateinit var cameraPermissionLauncher: ActivityResultLauncher private lateinit var locationPermissionLauncher: ActivityResultLauncher private var pendingType: String? = null private var pendingUri: Uri? = null private var permissionType: String? = null private var locationRequestType: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) repo = AttendanceRepository(this) faceAuth = FaceAuth(this) cameraLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK && pendingType != null && pendingUri != null) { val type = pendingType!! val path = try { pendingUri!!.toFile().path } catch (e: Exception) { pendingUri!!.path ?: return@registerForActivityResult } pendingType = null pendingUri = null lifecycleScope.launch { when (type) { "register" -> { faceAuth.saveFace(path) Toast.makeText(this@MainActivity, "Face registered", Toast.LENGTH_SHORT).show() } else -> { // Skip loading the image file for the demo build. The // liveness check currently always returns `true`, so // we can pass an empty list of frames. val live = faceAuth.runLivenessCheck(emptyList()) if (!live) { Toast.makeText(this@MainActivity, "Liveness check failed", Toast.LENGTH_SHORT).show() return@launch } val valid = faceAuth.verifyFace(path) if (valid) { repo.clock(type, path) } else { Toast.makeText(this@MainActivity, "Face verification failed", Toast.LENGTH_SHORT).show() } } } } } else { pendingType = null pendingUri = null } } cameraPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> val type = permissionType permissionType = null if (granted && type != null) { launchCamera(type) } else if (!granted) { Toast.makeText(this, "Camera permission required", Toast.LENGTH_SHORT).show() } } locationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> val type = locationRequestType locationRequestType = null if (granted && type != null) { checkLocationAndLaunch(type) } else if (!granted) { Toast.makeText(this, "Location permission required", Toast.LENGTH_SHORT).show() } } findViewById(R.id.clockInBtn).setOnClickListener { attemptClock("in") } findViewById(R.id.clockOutBtn).setOnClickListener { attemptClock("out") } findViewById(R.id.viewHoursBtn).setOnClickListener { startActivity(Intent(this, HoursActivity::class.java)) } findViewById(R.id.registerFaceBtn).setOnClickListener { launchCamera("register") } findViewById(R.id.payslipsBtn).setOnClickListener { startActivity(Intent(this, PayslipsActivity::class.java)) } findViewById(R.id.logoutBtn).setOnClickListener { repo.logout() startActivity(Intent(this, LoginActivity::class.java)) finish() } } override fun onResume() { super.onResume() lifecycleScope.launch { val list = repo.history() val adapter = android.widget.ArrayAdapter( this@MainActivity, android.R.layout.simple_list_item_1, list.map { "${it.type} - ${java.text.DateFormat.getTimeInstance().format(java.util.Date(it.time))}" } ) findViewById(R.id.historyList).adapter = adapter val status = if (faceAuth.isFaceRegistered()) "Face Registered" else "Face Not Registered" findViewById(R.id.statusText).text = status } } private fun attemptClock(type: String) { lifecycleScope.launch { val cal = java.util.Calendar.getInstance().apply { set(java.util.Calendar.HOUR_OF_DAY, 0) set(java.util.Calendar.MINUTE, 0) set(java.util.Calendar.SECOND, 0) set(java.util.Calendar.MILLISECOND, 0) } val last = repo.lastEntrySince(cal.timeInMillis) val allow = when (type) { "in" -> last == null || last.type == "out" "out" -> last != null && last.type == "in" else -> false } if (allow) { if (ContextCompat.checkSelfPermission(this@MainActivity, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { locationRequestType = type locationPermissionLauncher.launch(android.Manifest.permission.ACCESS_FINE_LOCATION) } else { checkLocationAndLaunch(type) } } else { val msg = if (type == "in") { "Already clocked in" } else { "No clock in found" } android.widget.Toast.makeText(this@MainActivity, msg, android.widget.Toast.LENGTH_SHORT).show() } } } private fun checkLocationAndLaunch(type: String) { lifecycleScope.launch { val locations = repo.getAuthorizedLocations() val current = repo.currentLocation() if (locations.isNotEmpty() && current != null) { var within = false var threshold = 50 for (loc in locations) { val results = FloatArray(1) android.location.Location.distanceBetween( current.latitude, current.longitude, loc.lat, loc.lng, results ) val radius = loc.radius ?: 50 threshold = kotlin.math.max(threshold, radius) if (results[0] <= radius.toFloat()) { within = true break } } if (!within) { androidx.appcompat.app.AlertDialog.Builder(this@MainActivity) .setMessage("You are more than $threshold meters from an authorized location. Continue?") .setPositiveButton("Yes") { _, _ -> launchCamera(type) } .setNegativeButton("No", null) .show() return@launch } } launchCamera(type) } } private fun launchCamera(type: String) { if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != android.content.pm.PackageManager.PERMISSION_GRANTED) { permissionType = type cameraPermissionLauncher.launch(android.Manifest.permission.CAMERA) return } val file = createImageFile(type) val uri = FileProvider.getUriForFile( this, "${BuildConfig.APPLICATION_ID}.fileprovider", file ) pendingType = type pendingUri = uri val intent = Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(android.provider.MediaStore.EXTRA_OUTPUT, uri) putExtra("android.intent.extras.CAMERA_FACING", 1) putExtra("android.intent.extras.LENS_FACING_FRONT", 1) putExtra("android.intent.extra.USE_FRONT_CAMERA", true) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } cameraLauncher.launch(intent) } private fun createImageFile(type: String): File { val dir = File(cacheDir, "images") if (!dir.exists()) dir.mkdirs() return if (type == "register") { val f = File(dir, "registered_face.jpg") if (f.exists()) f.delete() f.createNewFile() f } else { File.createTempFile("clock_${type}_", ".jpg", dir) } } }