511 lines
12 KiB
Markdown
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
|
|
|