# 🔧 Implementation Notes & Configuration Guide ## Lokasi Default Campus (UBH) Koordinat default yang digunakan dalam aplikasi: - **Latitude**: -6.2030 - **Longitude**: 107.0045 - **Radius**: 100 meter ### Cara Mengubah Lokasi Edit file `MainActivity.kt`, di fungsi `isWithinAbsensiRadius()`: ```kotlin fun isWithinAbsensiRadius( studentLat: Double, studentLon: Double, campusLat: Double = -6.2030, // ← UBAH INI campusLon: Double = 107.0045, // ← UBAH INI radiusMeters: Float = 100f // ← UBAH INI (dalam meter) ): Boolean { val distance = calculateDistance(studentLat, studentLon, campusLat, campusLon) return distance <= radiusMeters } ``` --- ## Webhook Configuration ### Webhook URL **Testing Endpoint**: ``` https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254 ``` **Production Endpoint**: ``` https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254 ``` ### Cara Mengganti Webhook URL Edit file `MainActivity.kt`, di fungsi `kirimKeN8n()`: ```kotlin fun kirimKeN8n( context: ComponentActivity, db: DatabaseHelper, npm: String, nama: String, latitude: Double, longitude: Double, foto: Bitmap ) { thread { try { // ...validation code... val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254") // ↑ UBAH URL DI SINI ↑ val conn = url.openConnection() as HttpURLConnection // ...rest of code... } } } ``` --- ## Privacy & Coordinate Obfuscation ### Current Implementation Saat ini, aplikasi mengirimkan koordinat asli ke server. Jika ingin menambahkan obfuscation: ```kotlin // Di dalam kirimKeN8n(), sebelum membuat JSON: val (obfuscatedLat, obfuscatedLon) = obfuscateCoordinates(latitude, longitude) val json = JSONObject().apply { put("npm", npm) put("nama", nama) put("latitude", obfuscatedLat) // ← Gunakan obfuscated put("longitude", obfuscatedLon) // ← Gunakan obfuscated put("timestamp", System.currentTimeMillis()) put("foto_base64", bitmapToBase64(foto)) put("validation_status", status) put("is_within_radius", isValidLocation) } ``` ### Customize Offset Edit nilai `offsetDegrees` dalam fungsi `obfuscateCoordinates()`: ```kotlin fun obfuscateCoordinates( latitude: Double, longitude: Double, offsetDegrees: Double = 0.002 // ← UBAH INI ): Pair { val randomLat = latitude + (Math.random() - 0.5) * offsetDegrees val randomLon = longitude + (Math.random() - 0.5) * offsetDegrees return Pair(randomLat, randomLon) } ``` **Offset Reference**: - 0.001 derajat ≈ 111 meter - 0.002 derajat ≈ 222 meter - 0.01 derajat ≈ 1.1 km --- ## Database Configuration ### Initial Database Setup Database otomatis dibuat pada first launch aplikasi di method `onCreate()` di `MainActivity`: ```kotlin class MainActivity : ComponentActivity() { private lateinit var db: DatabaseHelper override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) db = DatabaseHelper(this) // ← Database initialized here enableEdgeToEdge() // ... } } ``` ### Database Location SQLite database tersimpan di: ``` /data/data/id.ac.ubharajaya.sistemakademik/databases/Akademik.db ``` ### Debugging Database Menggunakan Android Studio: 1. Device Explorer → data → data → id.ac.ubharajaya.sistemakademik → databases 2. Download `Akademik.db` 3. Buka dengan SQLite browser Atau menggunakan adb: ```bash adb shell sqlite3 /data/data/id.ac.ubharajaya.sistemakademik/databases/Akademik.db SELECT * FROM attendance; ``` --- ## Photo Handling ### Current Implementation Foto diambil menggunakan intent: ```kotlin val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) cameraLauncher.launch(intent) ``` Foto disimpan sebagai Bitmap di memory dan dikonversi ke Base64 untuk dikirim. ### Limitations - Foto tidak disimpan ke storage permanent - Memory terbatas untuk foto besar - Foto hilang jika app closed ### Enhancement: Save to File Untuk menyimpan foto ke device storage: ```kotlin // Tambahkan di build.gradle.kts implementation("androidx.compose.material:material-icons-extended:1.6.0") // Update permission di AndroidManifest.xml // Update function untuk save foto fun saveFotoToFile(bitmap: Bitmap, npm: String): String { val filename = "absensi_${npm}_${System.currentTimeMillis()}.jpg" val file = File(context.getExternalFilesDir(null), filename) val fos = FileOutputStream(file) bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos) fos.close() return file.absolutePath } ``` --- ## Permission Handling ### Required Permissions Semua permissions sudah dideklarasikan di `AndroidManifest.xml`: ```xml ``` ### Runtime Permission Requests Aplikasi menggunakan `ActivityResultContracts` untuk request permissions di runtime: ```kotlin val locationPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { granted -> if (granted) { // Location permission granted } } val cameraPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { granted -> if (granted) { // Camera permission granted } } ``` --- ## Validation Logic ### Location Validation Flow ``` User clicks "Kirim Absensi" ↓ Check: latitude != null && longitude != null && foto != null ├─ NO → Show toast "Lokasi atau foto belum lengkap" └─ YES → kirimKeN8n() ↓ Validate location using isWithinAbsensiRadius() ├─ Within radius → status = "success" └─ Outside radius → status = "invalid_location" ↓ Save to database Send to webhook Show feedback toast ``` ### Custom Validation Untuk menambah validasi lainnya, edit `kirimKeN8n()`: ```kotlin fun kirimKeN8n( context: ComponentActivity, db: DatabaseHelper, npm: String, nama: String, latitude: Double, longitude: Double, foto: Bitmap ) { thread { try { // Custom validation 1: Check time val calendar = Calendar.getInstance() val hour = calendar.get(Calendar.HOUR_OF_DAY) if (hour < 7 || hour > 17) { // Absensi hanya boleh di jam 7-17 context.runOnUiThread { Toast.makeText(context, "Absensi hanya boleh jam 7-17", Toast.LENGTH_SHORT).show() } return@thread } // Custom validation 2: Location validation val isValidLocation = isWithinAbsensiRadius(latitude, longitude) val status = if (isValidLocation) "success" else "invalid_location" // Save & send... } } } ``` --- ## Error Handling ### Network Error Handling ```kotlin try { val url = URL(webhookUrl) val conn = url.openConnection() as HttpURLConnection conn.requestMethod = "POST" conn.setRequestProperty("Content-Type", "application/json") conn.doOutput = true // Send request... val responseCode = conn.responseCode context.runOnUiThread { val message = when { !isValidLocation -> "Absensi ditolak: Lokasi tidak sesuai" responseCode == 200 -> "Absensi diterima server" else -> "Absensi ditolak server" } Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } } catch (e: Exception) { context.runOnUiThread { Toast.makeText(context, "Gagal kirim ke server: ${e.message}", Toast.LENGTH_SHORT).show() } } ``` ### Improved Error Handling Tambahkan specific exception handling: ```kotlin catch (e: java.net.UnknownHostException) { // Network not available Toast.makeText(context, "Tidak ada koneksi internet", Toast.LENGTH_SHORT).show() } catch (e: java.net.SocketTimeoutException) { // Server timeout Toast.makeText(context, "Server tidak merespons (timeout)", Toast.LENGTH_SHORT).show() } catch (e: Exception) { // Other errors Toast.makeText(context, "Error: ${e.localizedMessage}", Toast.LENGTH_SHORT).show() } ``` --- ## Logging & Debugging ### Add Logging ```kotlin import android.util.Log class MainActivity : ComponentActivity() { companion object { private const val TAG = "EAS_DEBUG" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d(TAG, "MainActivity created") // ... } } fun kirimKeN8n(...) { thread { try { Log.d(TAG, "Starting attendance submission") Log.d(TAG, "Location: $latitude, $longitude") Log.d(TAG, "Validation status: $status") // ... rest of code ... Log.d(TAG, "Response code: $responseCode") } } } ``` ### View Logs ```bash adb logcat | grep EAS_DEBUG ``` --- ## Performance Optimization ### Threading Semua network operations sudah di thread terpisah: ```kotlin thread { // Long-running operations tidak akan freeze UI val url = URL(webhookUrl) val conn = url.openConnection() as HttpURLConnection // ... network code ... } ``` ### Bitmap Compression Foto sudah dikompres sebelum convert ke Base64: ```kotlin fun bitmapToBase64(bitmap: Bitmap): String { val outputStream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) // 80% quality return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) } ``` --- ## Build & Release ### Build Configuration ```gradle android { compileSdk = 36 minSdk = 28 targetSdk = 36 buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } } ``` ### Build APK ```bash ./gradlew clean assembleDebug // Debug APK ./gradlew clean assembleRelease // Release APK ``` APK akan tersimpan di: `app/build/outputs/apk/` --- ## Useful Commands ### Clear App Data ```bash adb shell pm clear id.ac.ubharajaya.sistemakademik ``` ### View Database ```bash adb pull /data/data/id.ac.ubharajaya.sistemakademik/databases/Akademik.db ``` ### Install APK ```bash adb install -r app/build/outputs/apk/debug/app-debug.apk ``` ### View Logs ```bash adb logcat id.ac.ubharajaya.sistemakademik:V *:S ``` --- ## Testing Scenarios ### Test Case 1: Happy Path 1. Register user 2. Login 3. Grant permissions 4. Di lokasi yang valid 5. Ambil foto 6. Kirim absensi 7. ✅ Absensi diterima 8. Cek di riwayat ### Test Case 2: Invalid Location 1. Login 2. Di lokasi yang TIDAK valid (>100m dari campus) 3. Ambil foto 4. Kirim absensi 5. ✅ Absensi ditolak dengan pesan lokasi 6. Cek status di database = "invalid_location" ### Test Case 3: Permission Denied 1. Login 2. Deny location permission 3. ✅ Show dialog atau request ulang ### Test Case 4: Network Error 1. Turn off WiFi & Mobile data 2. Coba kirim absensi 3. ✅ Show error message 4. Data tetap tersimpan di database lokal --- **Last Updated**: 14 January 2026