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 import com.taf.attendance.sync.NetworkMonitor class MainActivity : AppCompatActivity() { private lateinit var repo: AttendanceRepository private lateinit var faceAuth: FaceAuth private lateinit var networkMonitor: NetworkMonitor 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 pendingFile: File? = 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, repo) // Initialize network monitoring and background sync initializeSync() cameraLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK && pendingType != null && pendingFile != null) { val type = pendingType!! val path = pendingFile!!.path pendingType = null pendingUri = null pendingFile = null lifecycleScope.launch { when (type) { "register" -> { val saved = faceAuth.saveFace(repo.getUserId(), path) val msg = if (saved) "Face registered" else "Face registration failed" Toast.makeText(this@MainActivity, msg, Toast.LENGTH_SHORT).show() updateFaceStatus() } 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 id = repo.getUserId() val valid = faceAuth.verifyFace(id, path) if (valid) { val result = repo.clock(type, path) if (result.success) { loadHistory() updateSyncStatus() val time = java.text.DateFormat.getTimeInstance() .format(java.util.Date()) if (!result.withinRange) { // Show warning but still allow clocking val distance = result.distanceToNearest?.let { "%.0f".format(it) } ?: "unknown" androidx.appcompat.app.AlertDialog.Builder(this@MainActivity) .setTitle("Location Warning") .setMessage("You are ${distance}m from the nearest authorized branch. Clock ${type} recorded but location is outside allowed area.") .setPositiveButton("OK") { _, _ -> Toast.makeText( this@MainActivity, "Clock ${type} recorded at: $time", Toast.LENGTH_SHORT ).show() } .setCancelable(false) .show() } else { Toast.makeText( this@MainActivity, "Clock ${type} successful at: $time", Toast.LENGTH_SHORT ).show() } } else { Toast.makeText( this@MainActivity, "Failed to clock $type", Toast.LENGTH_SHORT ).show() } } else { repo.reportFaceFailure(id, repo.getUserFace(id)?.path, path) Toast.makeText( this@MainActivity, "Face verification failed", Toast.LENGTH_SHORT ).show() } } } } } else { pendingType = null pendingUri = null pendingFile = 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") } // Hide/show payslips button based on build config val payslipsBtn = findViewById(R.id.payslipsBtn) if (BuildConfig.ENABLE_PAYSLIPS) { payslipsBtn.setOnClickListener { startActivity(Intent(this, PayslipsActivity::class.java)) } } else { payslipsBtn.visibility = android.view.View.GONE } // Hide/show leave button based on build config val leaveBtn = findViewById(R.id.leaveBtn) if (BuildConfig.ENABLE_LEAVE) { leaveBtn.setOnClickListener { startActivity(Intent(this, LeaveActivity::class.java)) } } else { leaveBtn.visibility = android.view.View.GONE } findViewById(R.id.logoutBtn).setOnClickListener { lifecycleScope.launch { repo.logout() startActivity(Intent(this@MainActivity, LoginActivity::class.java)) finish() } } } private fun initializeSync() { // Initialize network monitor networkMonitor = NetworkMonitor.getInstance(this) // Schedule periodic sync for offline records repo.schedulePeriodicSync() // Observe network changes and trigger sync when online networkMonitor.observe(this) { isOnline -> lifecycleScope.launch { if (isOnline) { val unsyncedCount = repo.getUnsyncedCount() if (unsyncedCount > 0) { android.util.Log.d("MainActivity", "Device online with $unsyncedCount unsynced records, triggering sync") repo.triggerImmediateSync() } } updateSyncStatus() } } } override fun onResume() { super.onResume() lifecycleScope.launch { loadHistory() updateFaceStatus() updateSyncStatus() } } private fun attemptClock(type: String) { lifecycleScope.launch { // Try to get last entry from backend first (ensures cross-device consistency) var last = repo.getLastEntryFromBackend() // Fallback to local check if backend unavailable (offline mode) if (last == null) { 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) } 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 branches = repo.getAuthorizedBranches() val current = repo.currentLocation() // Only check location if we have branches with GPS coordinates if (branches.isNotEmpty() && current != null) { val branchesWithGPS = branches.filter { it.latitude != null && it.longitude != null } if (branchesWithGPS.isNotEmpty()) { // GPS validation is possible var within = false var nearestDistance = Float.MAX_VALUE val threshold = 50 // Default 50m radius for branches for (branch in branchesWithGPS) { val results = FloatArray(1) android.location.Location.distanceBetween( current.latitude, current.longitude, branch.latitude!!, branch.longitude!!, results ) if (results[0] <= threshold.toFloat()) { within = true break } if (results[0] < nearestDistance) { nearestDistance = results[0] } } if (!within) { // Show warning but allow proceeding val distance = if (nearestDistance < Float.MAX_VALUE) "%.0f".format(nearestDistance) else "unknown" androidx.appcompat.app.AlertDialog.Builder(this@MainActivity) .setTitle("Location Warning") .setMessage("You are ${distance}m from the nearest authorized branch. Continue with clock $type?") .setPositiveButton("Yes") { _, _ -> launchCamera(type) } .setNegativeButton("No", null) .show() return@launch } } // If no branches have GPS coordinates, skip validation and proceed } // If no branches at all or no location available, proceed normally 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 pendingFile = file 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) } } private suspend fun loadHistory() { 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 } private suspend fun updateFaceStatus() { val status = if (faceAuth.isFaceRegistered(repo.getUserId())) { "Face Registered" } else { "Face Not Registered" } findViewById(R.id.statusText).text = status } private suspend fun updateSyncStatus() { val unsyncedCount = repo.getUnsyncedCount() val isOnline = networkMonitor.isNetworkAvailable() val status = when { unsyncedCount == 0 -> "✓ All data synced" isOnline -> "🔄 Syncing $unsyncedCount pending records..." else -> "⚠️ $unsyncedCount pending records (offline)" } findViewById(R.id.syncStatusText).text = status } }