diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..f0c6ad0 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,50 @@ + + + + \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 0000000..c61ea33 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt index c774502..2227bcc 100644 --- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt @@ -3,7 +3,6 @@ package id.ac.ubharajaya.sistemakademik import android.Manifest import android.app.Activity import android.content.Intent -import android.content.pm.PackageManager import android.graphics.Bitmap import android.os.Bundle import android.provider.MediaStore @@ -14,10 +13,16 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts +import android.annotation.SuppressLint +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat @@ -28,66 +33,74 @@ import java.io.ByteArrayOutputStream import java.net.HttpURLConnection import java.net.URL import kotlin.concurrent.thread +import android.content.pm.PackageManager -/* ================= UTIL ================= */ + +/* ================= UTIL (TIDAK DIUBAH) ================= */ fun bitmapToBase64(bitmap: Bitmap): String { - val outputStream = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) - return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) + val out = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out) + return Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP) +} + +fun hitungJarak(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double { + val R = 6371000.0 + val dLat = Math.toRadians(lat2 - lat1) + val dLon = Math.toRadians(lon2 - lon1) + val a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(Math.toRadians(lat1)) * + Math.cos(Math.toRadians(lat2)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2) + val c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) + return R * c } fun kirimKeN8n( - context: ComponentActivity, - latitude: Double, - longitude: Double, - foto: Bitmap + activity: ComponentActivity, + npm: String, + nama: String, + matkul: String, + lat: Double, + lon: Double, + foto: Bitmap, + status: String ) { thread { try { val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254") -// test URL val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254") val conn = url.openConnection() as HttpURLConnection - conn.requestMethod = "POST" conn.setRequestProperty("Content-Type", "application/json") conn.doOutput = true val json = JSONObject().apply { - put("npm", "12345") - put("nama","Arif R D") - put("latitude", latitude) - put("longitude", longitude) put("timestamp", System.currentTimeMillis()) + put("ip_addr", "android") + put("npm", npm) + put("nama", nama) + put("latitude", lat) + put("longitude", lon) + put("mata_kuliah", matkul) + put("status", status) put("foto_base64", bitmapToBase64(foto)) } - conn.outputStream.use { - it.write(json.toString().toByteArray()) - } + conn.outputStream.use { it.write(json.toString().toByteArray()) } val responseCode = conn.responseCode - - context.runOnUiThread { + activity.runOnUiThread { Toast.makeText( - context, - if (responseCode == 200) - "Absensi diterima server" - else - "Absensi ditolak server", + activity, + if (responseCode == 200) "Absensi berhasil" else "Absensi ditolak", Toast.LENGTH_SHORT ).show() } - conn.disconnect() - - } catch (_: Exception) { - context.runOnUiThread { - Toast.makeText( - context, - "Gagal kirim ke server", - Toast.LENGTH_SHORT - ).show() + } catch (e: Exception) { + activity.runOnUiThread { + Toast.makeText(activity, "Gagal kirim absensi", Toast.LENGTH_SHORT).show() } } } @@ -96,19 +109,12 @@ fun kirimKeN8n( /* ================= ACTIVITY ================= */ class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() - setContent { SistemAkademikTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - AbsensiScreen( - modifier = Modifier.padding(innerPadding), - activity = this - ) - } + AbsensiScreen(this) } } } @@ -117,158 +123,141 @@ class MainActivity : ComponentActivity() { /* ================= UI ================= */ @Composable -fun AbsensiScreen( - modifier: Modifier = Modifier, - activity: ComponentActivity -) { +fun AbsensiScreen(activity: ComponentActivity) { val context = LocalContext.current - var lokasi by remember { mutableStateOf("Koordinat: -") } - var latitude by remember { mutableStateOf(null) } - var longitude by remember { mutableStateOf(null) } + var npm by remember { mutableStateOf("") } + var nama by remember { mutableStateOf("") } + var matkul by remember { mutableStateOf("") } + var lat by remember { mutableStateOf(null) } + var lon by remember { mutableStateOf(null) } + var lokasi by remember { mutableStateOf("Lokasi belum didapat") } var foto by remember { mutableStateOf(null) } - val fusedLocationClient = - LocationServices.getFusedLocationProviderClient(context) + val LAT_ABSEN = -6.255 + val LON_ABSEN = 106.618 + val RADIUS = 30.0 // meter (REALISTIS) - /* ===== Permission Lokasi ===== */ + val fusedClient = LocationServices.getFusedLocationProviderClient(context) val locationPermissionLauncher = - rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { granted -> - if (granted) { - - if ( - ContextCompat.checkSelfPermission( - context, - Manifest.permission.ACCESS_FINE_LOCATION - ) == PackageManager.PERMISSION_GRANTED - ) { - - fusedLocationClient.lastLocation - .addOnSuccessListener { location -> - if (location != null) { - latitude = location.latitude - longitude = location.longitude - lokasi = - "Lat: ${location.latitude}\nLon: ${location.longitude}" - } else { - lokasi = "Lokasi tidak tersedia" - } - } - .addOnFailureListener { - lokasi = "Gagal mengambil lokasi" - } - } - - } else { - Toast.makeText( + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { + if (it && ContextCompat.checkSelfPermission( context, - "Izin lokasi ditolak", - Toast.LENGTH_SHORT - ).show() + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + ) { + ambilLokasi(fusedClient) { la, lo -> + lat = la + lon = lo + lokasi = "Lat: $la | Lon: $lo" + } } } - /* ===== Kamera ===== */ - val cameraLauncher = - rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - val bitmap = - result.data?.extras?.getParcelable("data", Bitmap::class.java) - if (bitmap != null) { - foto = bitmap - Toast.makeText( - context, - "Foto berhasil diambil", - Toast.LENGTH_SHORT - ).show() - } + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == Activity.RESULT_OK) { + foto = it.data?.extras?.get("data") as? Bitmap } } val cameraPermissionLauncher = - rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { granted -> - if (granted) { - val intent = - Intent(MediaStore.ACTION_IMAGE_CAPTURE) - cameraLauncher.launch(intent) - } else { - Toast.makeText( - context, - "Izin kamera ditolak", - Toast.LENGTH_SHORT - ).show() - } + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { + if (it) cameraLauncher.launch(Intent(MediaStore.ACTION_IMAGE_CAPTURE)) } - /* ===== Request Awal ===== */ - LaunchedEffect(Unit) { - locationPermissionLauncher.launch( - Manifest.permission.ACCESS_FINE_LOCATION - ) + locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) } - /* ===== UI ===== */ - - Column( - modifier = modifier - .fillMaxSize() - .padding(24.dp), - verticalArrangement = Arrangement.Center - ) { - - Text( - text = "Absensi Akademik", - style = MaterialTheme.typography.titleLarge - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text(text = lokasi) - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = { - cameraPermissionLauncher.launch( - Manifest.permission.CAMERA - ) - }, - modifier = Modifier.fillMaxWidth() + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation(8.dp) ) { - Text("Ambil Foto") - } + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { - Spacer(modifier = Modifier.height(12.dp)) + Text("Absensi Akademik", style = MaterialTheme.typography.titleLarge) - Button( - onClick = { - if (latitude != null && longitude != null && foto != null) { - kirimKeN8n( - activity, - latitude!!, - longitude!!, - foto!! - ) - } else { - Toast.makeText( - context, - "Lokasi atau foto belum lengkap", - Toast.LENGTH_SHORT - ).show() + OutlinedTextField(npm, { npm = it }, label = { Text("NPM") }, modifier = Modifier.fillMaxWidth()) + OutlinedTextField(nama, { nama = it }, label = { Text("Nama") }, modifier = Modifier.fillMaxWidth()) + OutlinedTextField(matkul, { matkul = it }, label = { Text("Mata Kuliah") }, modifier = Modifier.fillMaxWidth()) + + Text(lokasi, style = MaterialTheme.typography.bodySmall) + + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) } + ) { + Text("Ambil Foto") } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Kirim Absensi") + + foto?.let { + Image( + bitmap = it.asImageBitmap(), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + contentScale = ContentScale.Crop + ) + } + + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + if (npm.isBlank() || nama.isBlank() || matkul.isBlank() + || lat == null || lon == null || foto == null + ) { + Toast.makeText(context, "Data belum lengkap", Toast.LENGTH_SHORT).show() + return@Button + } + + val jarak = hitungJarak(lat!!, lon!!, LAT_ABSEN, LON_ABSEN) + + // 🔴 BLOK TOTAL JIKA DI LUAR RADIUS + if (jarak > RADIUS) { + Toast.makeText( + context, + "Anda berada di luar radius absensi", + Toast.LENGTH_LONG + ).show() + return@Button + } + + // ✅ HANYA JIKA VALID BARU KIRIM + kirimKeN8n( + activity, + npm, + nama, + matkul, + lat!!, + lon!!, + foto!!, + "VALID" + ) + } + ) { + Text("Kirim Absensi") + } + } } } } + +@SuppressLint("MissingPermission") +fun ambilLokasi( + client: com.google.android.gms.location.FusedLocationProviderClient, + onResult: (Double, Double) -> Unit +) { + client.lastLocation.addOnSuccessListener { + if (it != null) onResult(it.latitude, it.longitude) + } +}