202310715130-Dwifebbryanti-EAS/IMPLEMENTATION_NOTES.md
2026-01-14 21:33:58 +07:00

12 KiB

🔧 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():

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():

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:

// 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():

fun obfuscateCoordinates(
    latitude: Double,
    longitude: Double,
    offsetDegrees: Double = 0.002  // ← UBAH INI
): Pair<Double, Double> {
    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:

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:

adb shell
sqlite3 /data/data/id.ac.ubharajaya.sistemakademik/databases/Akademik.db
SELECT * FROM attendance;

Photo Handling

Current Implementation

Foto diambil menggunakan intent:

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:

// Tambahkan di build.gradle.kts
implementation("androidx.compose.material:material-icons-extended:1.6.0")

// Update permission di AndroidManifest.xml
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

// 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:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.CAMERA"/>

Runtime Permission Requests

Aplikasi menggunakan ActivityResultContracts untuk request permissions di runtime:

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():

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

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:

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

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

adb logcat | grep EAS_DEBUG

Performance Optimization

Threading

Semua network operations sudah di thread terpisah:

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:

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

android {
    compileSdk = 36
    minSdk = 28
    targetSdk = 36
    
    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

Build APK

./gradlew clean assembleDebug    // Debug APK
./gradlew clean assembleRelease  // Release APK

APK akan tersimpan di: app/build/outputs/apk/


Useful Commands

Clear App Data

adb shell pm clear id.ac.ubharajaya.sistemakademik

View Database

adb pull /data/data/id.ac.ubharajaya.sistemakademik/databases/Akademik.db

Install APK

adb install -r app/build/outputs/apk/debug/app-debug.apk

View Logs

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