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

511 lines
12 KiB
Markdown

# 🔧 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<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`:
```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
<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`:
```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:
```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