Compare commits

..

27 Commits

Author SHA1 Message Date
30e5a80e4a Fitur Koordinat Lokasi dengan GPS dan Geofencing 2025-12-03 11:48:58 +07:00
835b856792 Penyesuaian Code Settings Menu 2025-12-03 10:35:13 +07:00
3a5911c69f Merge remote-tracking branch 'origin/kelompok-1' into kelompok-1 2025-12-03 10:11:14 +07:00
dend
8a0bef9d0d Add NTFY configuration settings and persistence using SharedPreferences
Komit ini mengimplementasikan fitur konfigurasi endpoint NTFY yang dinamis dan menyimpannya menggunakan SharedPreferences. Ini mencakup penambahan layar pengaturan (SettingsScreen), perbaikan navigasi kembali menggunakan BackHandler, dan pembaruan fungsi sendNotification agar membaca URL dari konfigurasi yang disimpan.
2025-11-28 00:36:06 +07:00
dend
358eb6e860 Add NTFY configuration settings and persistence using SharedPreferences
Komit ini mengimplementasikan fitur konfigurasi endpoint NTFY yang dinamis dan menyimpannya menggunakan SharedPreferences. Ini mencakup penambahan layar pengaturan (SettingsScreen), perbaikan navigasi kembali menggunakan BackHandler, dan pembaruan fungsi sendNotification agar membaca URL dari konfigurasi yang disimpan.
2025-11-28 00:28:10 +07:00
dend
e2bd34be8a Add NTFY configuration settings and persistence using SharedPreferences
Komit ini mengimplementasikan fitur konfigurasi endpoint NTFY yang dinamis dan menyimpannya menggunakan SharedPreferences. Ini mencakup penambahan layar pengaturan (SettingsScreen), perbaikan navigasi kembali menggunakan BackHandler, dan pembaruan fungsi sendNotification agar membaca URL dari konfigurasi yang disimpan.
2025-11-28 00:26:58 +07:00
0208835ce3 Revert "Menyesuaikan"
This reverts commit 36607418
2025-11-27 21:56:06 +07:00
9807c900dd Menambahkan Activity di Andorid Manifest 2025-11-27 15:51:42 +07:00
327c9048a4 Merge remote-tracking branch 'origin/kelompok-1' into kelompok-1 2025-11-27 15:49:39 +07:00
26d3f77b7a Menambahkan Fitur Konfigurasi Pemilihan Server dan Topik pada SharedReferences 2025-11-27 15:48:49 +07:00
088ac12813 Delete SwiftVersion/Icon Aplikasi.jpg 2025-11-27 15:42:33 +07:00
ebc2bce6da Upload files to "SwiftVersion" 2025-11-27 15:42:16 +07:00
366074187a Menyesuaikan 2025-11-20 21:08:33 +07:00
b26ebf147a Merge remote-tracking branch 'origin/kelompok-1' into kelompok-1
# Conflicts:
#	app/src/main/java/id/ac/ubharajaya/panicbutton/MainActivity.kt
2025-11-20 15:47:10 +07:00
1c158449a4 Mengubah Tag sesuai Kiriman User dan Menambahkan Activity baru untuk Jalur Evakuasi 2025-11-20 15:46:11 +07:00
55a06a5312 Kelompok 1 2025-11-20 15:39:12 +07:00
48d1205ba8 Mengubah Tag sesuai Kiriman User dan Menambahkan Activity baru untuk Jalur Evakuasi 2025-11-20 15:38:38 +07:00
8b5a91bce1 Emojis Test 2025-11-13 15:48:41 +07:00
e855ff870c Emojis Test 2025-11-13 15:45:29 +07:00
50fa900a81 Emojis Test 2025-11-13 15:40:13 +07:00
5103e988cf Emojis Test 2025-11-13 15:37:13 +07:00
d273dfbd44 Image Button 2025-11-13 15:19:33 +07:00
0bd6dfe8c0 Progress Praktikum-1 2025-11-13 15:10:18 +07:00
86ba20f78c Progress Praktikum-1 2025-11-13 15:06:36 +07:00
362a79d591 Merge remote-tracking branch 'origin/master' 2025-11-13 14:28:37 +07:00
1b80313eeb Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2025-11-13 14:28:01 +07:00
84d3831f7f Add name at README.md 2025-11-13 14:27:45 +07:00
10 changed files with 884 additions and 64 deletions

10
.idea/deploymentTargetSelector.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

View File

@ -9,28 +9,21 @@ Repository ini digunakan untuk praktikum perkuliahan pemrograman mobile.
- Arif Dwiyanto: arif.dwiyanto@ubharajaya.ac.id
## Mahasiswa
- 1. Dendi Yogia Pratama (202310715051)
- 2. Nuryuda Maulana (202310715038)
- 3. Haga Dalpinto Ginting (202310715176)
- 4. Rafi Fattan Fitriardi (202310715002)
- 5. Fadlan Rivaldi (202310715280)
- 6. RAKHA ADI SAPUTRO (202310715083)
- 7. Arif Nurkhayan (202310715128)
- 8. Fazri Abdurrahman (202310715082)
- 9. Markco Van Nistelrooy Sitanggang (202310715181)
- 10. Muhammad Fadzel Hadean Rukrus (202310715220)
- 11. Yosep Gamaliel Mulia (202310715105)
- 12. Satrio Putra Wardani (202310715307)
- 13. Faris Naufal Priatna (202310715123)
- 14. Nabila suwandira (202310715066)
- 15. Indris Alpasela (202310715200)
- 16. Raihan Ariq Muzakki (202310715297)
- 17. Dirson Ali Wardana (202310715246)
- 18. Dimas Hendri Pamungkas (202310715274)
- 19. Fadhlul Wafi (202310715188)
- 20. Muhammad Yusron Amrullah (202310715060)
- 21. Muhammad Fadillah (202310715213)
- 22. Hadi Guna Prakoso (202310715312)
- 23. Muhammad Rafi (202310715191)
- 24. Muhammad Rafly Al-Fathir (202310715043)
- 25. Jeremia Sebastian Marpaung (202310715096)
- A.Dendi Yogia Pratama(202310715051)
- B.Nuryuda Maulana(202310715038)
- C Haga Dalpinto Ginting(202310715176)
- D Rafi Fattan Fitriardi (202310715002)
- E. Fadlan Rivaldi (202310715280)
- F. RAKHA ADI SAPUTRO (202310715083)
- G. Arif Nurkhayan (202310715128)
- H. Fazri Abdurrahman(202310715082)
- I. Markco Van Nistelrooy Sitanggang (202310715181)
- J.Muhammad Fadzel Hadean Rukrus (202310715220)
- K. Yosep Gamaliel Mulia (202310715105)
- L.Satrio Putra Wardani(202310715307)
- M.Faris Naufal Priatna(202310715123)
- N. Nabila suwandira(202310715066)
- O. Indris Alpasela(202310715200)
- P. Raihan Ariq Muzakki (202310715297)
- Q. Dirson Ali Wardana (202310715246)
- R. Dimas Hendri Pamungkas (202310715274)

View File

@ -6,9 +6,7 @@ plugins {
android {
namespace = "id.ac.ubharajaya.panicbutton"
compileSdk {
version = release(36)
}
compileSdk = 36
defaultConfig {
applicationId = "id.ac.ubharajaya.panicbutton"
@ -42,14 +40,33 @@ android {
}
dependencies {
// OkHttp untuk HTTP request
implementation("com.squareup.okhttp3:okhttp:4.11.0")
// Coroutines untuk manajemen thread
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
// Google Play Services Location - UNTUK GPS & GEOFENCING
implementation("com.google.android.gms:play-services-location:21.0.1")
// Core AndroidX dan Compose BOM
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
// Compose UI & Material3
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.foundation)
// Material Icons Extended - untuk icon LocationOn
implementation("androidx.compose.material:material-icons-extended:1.5.4")
// Tools dan Testing
implementation(libs.androidx.compose.ui.tooling.preview)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@ -57,8 +74,4 @@ dependencies {
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
//praktikum 1
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("androidx.compose.material3:material3:1.1.1")
}

View File

@ -2,6 +2,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Permission untuk Internet -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Permission untuk Lokasi -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@ -10,18 +17,28 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PanicButton">
android:theme="@style/Theme.PanicButton"
android:usesCleartextTraffic="true"
>
<activity
android:name=".MainActivity"
android:name="id.ac.ubharajaya.panicbutton.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.PanicButton">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".JalurEvakuasiActivity"
android:exported="false" />
<activity android:name=".AlertActivity"
android:exported="false" />
</application>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@ -0,0 +1,167 @@
package id.ac.ubharajaya.panicbutton
import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
// ----------------------------------------------------------------------
// 1. CONSTANTS & HELPER FUNCTIONS FOR SHAREDPREFERENCES
// ----------------------------------------------------------------------
const val PREFS_NAME = "AppPrefs"
const val KEY_NTFY_SERVER = "ntfyServer"
const val KEY_NTFY_TOPIC = "ntfyTopic"
const val DEFAULT_NTFY_SERVER = "https://ntfy.ubharajaya.ac.id"
const val DEFAULT_NTFY_TOPIC = "panic-button"
fun getPrefs(context: Context) = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
fun saveNtfyConfig(context: Context, server: String, topic: String) {
getPrefs(context).edit().apply {
putString(KEY_NTFY_SERVER, server)
putString(KEY_NTFY_TOPIC, topic)
apply()
}
}
fun getNtfyUrl(context: Context): String {
val prefs = getPrefs(context)
val server = prefs.getString(KEY_NTFY_SERVER, DEFAULT_NTFY_SERVER)
val topic = prefs.getString(KEY_NTFY_TOPIC, DEFAULT_NTFY_TOPIC)
val cleanServer = server?.trimEnd('/') ?: DEFAULT_NTFY_SERVER.trimEnd('/')
val cleanTopic = topic?.trimStart('/') ?: DEFAULT_NTFY_TOPIC.trimStart('/')
return "$cleanServer/$cleanTopic"
}
class AlertActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SettingsScreen(onBack = { finish() })
}
}
}
// ----------------------------------------------------------------------
// SETTINGS SCREEN
// ----------------------------------------------------------------------
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(onBack: () -> Unit) {
val context = LocalContext.current
val prefs = remember { getPrefs(context) }
// Inisialisasi state dari SharedPreferences
var serverText by remember {
mutableStateOf(TextFieldValue(prefs.getString(KEY_NTFY_SERVER, DEFAULT_NTFY_SERVER) ?: DEFAULT_NTFY_SERVER))
}
var topicText by remember {
mutableStateOf(TextFieldValue(prefs.getString(KEY_NTFY_TOPIC, DEFAULT_NTFY_TOPIC) ?: DEFAULT_NTFY_TOPIC))
}
var saveMessage by remember { mutableStateOf("") }
// ⬅️ TAMBAHAN: Menangani tombol kembali pada perangkat
BackHandler(onBack = onBack)
Scaffold(
topBar = {
TopAppBar(
title = { Text("Pengaturan NTFY Endpoint") },
navigationIcon = {
IconButton(onClick = onBack) { // ⬅️ onBack menangani navigasi kembali ke MainScreen
Icon(Icons.Filled.ArrowBack, contentDescription = "Kembali")
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.Start
) {
Text(
text = "Konfigurasi Server Notifikasi",
fontSize = 18.sp,
modifier = Modifier.padding(bottom = 16.dp)
)
OutlinedTextField(
value = serverText,
onValueChange = { serverText = it },
label = { Text("Server NTFY (contoh: https://ntfy.sh)") },
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
singleLine = true
)
OutlinedTextField(
value = topicText,
onValueChange = { topicText = it },
label = { Text("Topic (contoh: panic-button)") },
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
singleLine = true
)
Button(
onClick = {
saveNtfyConfig(
context,
serverText.text.trim(),
topicText.text.trim()
)
saveMessage = "Konfigurasi berhasil disimpan! URL: ${getNtfyUrl(context)}"
},
modifier = Modifier.fillMaxWidth()
) {
Text("Simpan Konfigurasi")
}
Text(
text = saveMessage,
color = if (saveMessage.contains("berhasil")) Color.Green else Color.Black,
modifier = Modifier.padding(top = 16.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "URL saat ini: ${getNtfyUrl(context)}",
style = TextStyle(color = Color.Gray, fontSize = 12.sp)
)
}
}
}

View File

@ -0,0 +1,62 @@
package id.ac.ubharajaya.panicbutton
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
class JalurEvakuasiActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JalurEvakuasiScreen(onClose = { finish() })
}
}
}
@Composable
fun JalurEvakuasiScreen(onClose: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Jalur Evakuasi",
fontSize = 20.sp,
)
Spacer(modifier = Modifier.height(16.dp))
// Menampilkan gambar denah
Image(
painter = painterResource(id = R.drawable.jalur_evakuasi),
contentDescription = "Denah Jalur Evakuasi",
modifier = Modifier
.fillMaxWidth()
.height(450.dp)
)
Spacer(modifier = Modifier.height(20.dp))
// Tombol Close
Button(
onClick = { onClose() },
colors = ButtonDefaults.buttonColors(containerColor = Color.Red)
) {
Text(text = "Close", color = Color.White)
}
}
}

View File

@ -0,0 +1,214 @@
package id.ac.ubharajaya.panicbutton
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.os.Looper
import androidx.core.app.ActivityCompat
import com.google.android.gms.location.*
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
/**
* LocationManager untuk handle GPS tracking dan Geofencing
* Kampus Ubhara Jaya: -6.223276, 107.009273 (Radius: 500m)
*/
class LocationManager(private val context: Context) {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
// Koordinat Kampus Ubhara Jaya
companion object {
private const val CAMPUS_LAT = -6.223276
private const val CAMPUS_LNG = 107.009273
private const val CAMPUS_RADIUS_METERS = 500.0
}
/**
* Data class untuk menyimpan informasi lokasi
*/
data class LocationData(
val latitude: Double,
val longitude: Double,
val isInsideCampus: Boolean,
val distanceFromCampus: Double // dalam meter
)
/**
* Cek apakah permission lokasi sudah diberikan
*/
fun hasLocationPermission(): Boolean {
return ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
/**
* Get lokasi sekali (one-time location)
*/
fun getCurrentLocation(onResult: (LocationData?) -> Unit) {
if (!hasLocationPermission()) {
onResult(null)
return
}
try {
fusedLocationClient.lastLocation.addOnSuccessListener { location: Location? ->
if (location != null) {
val locationData = processLocation(location)
onResult(locationData)
} else {
// Jika lastLocation null, request location update sekali
requestSingleLocationUpdate(onResult)
}
}.addOnFailureListener {
onResult(null)
}
} catch (e: SecurityException) {
onResult(null)
}
}
/**
* Request location update sekali jika lastLocation tidak tersedia
*/
private fun requestSingleLocationUpdate(onResult: (LocationData?) -> Unit) {
if (!hasLocationPermission()) {
onResult(null)
return
}
val locationRequest = LocationRequest.Builder(
Priority.PRIORITY_HIGH_ACCURACY,
5000L // 5 detik
).apply {
setMaxUpdates(1) // Hanya 1 kali update
}.build()
val locationCallback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
val location = result.lastLocation
if (location != null) {
val locationData = processLocation(location)
onResult(locationData)
} else {
onResult(null)
}
fusedLocationClient.removeLocationUpdates(this)
}
}
try {
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
} catch (e: SecurityException) {
onResult(null)
}
}
/**
* Real-time location monitoring menggunakan Flow
* Untuk digunakan saat app aktif
*/
fun getLocationUpdates(): Flow<LocationData?> = callbackFlow {
if (!hasLocationPermission()) {
trySend(null)
close()
return@callbackFlow
}
val locationRequest = LocationRequest.Builder(
Priority.PRIORITY_HIGH_ACCURACY,
10000L // Update setiap 10 detik
).apply {
setMinUpdateIntervalMillis(5000L) // Minimal 5 detik
setMaxUpdateDelayMillis(15000L) // Maksimal delay 15 detik
}.build()
val locationCallback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.lastLocation?.let { location ->
val locationData = processLocation(location)
trySend(locationData)
}
}
}
try {
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
} catch (e: SecurityException) {
trySend(null)
}
awaitClose {
fusedLocationClient.removeLocationUpdates(locationCallback)
}
}
/**
* Process location dan cek apakah di dalam kampus
*/
private fun processLocation(location: Location): LocationData {
val distance = calculateDistance(
location.latitude,
location.longitude,
CAMPUS_LAT,
CAMPUS_LNG
)
val isInsideCampus = distance <= CAMPUS_RADIUS_METERS
return LocationData(
latitude = location.latitude,
longitude = location.longitude,
isInsideCampus = isInsideCampus,
distanceFromCampus = distance
)
}
/**
* Hitung jarak antara 2 koordinat menggunakan Haversine formula
* Return: jarak dalam meter
*/
private fun calculateDistance(
lat1: Double,
lon1: Double,
lat2: Double,
lon2: Double
): Double {
val earthRadius = 6371000.0 // Radius bumi dalam meter
val dLat = Math.toRadians(lat2 - lat1)
val dLon = Math.toRadians(lon2 - lon1)
val a = sin(dLat / 2) * sin(dLat / 2) +
cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) *
sin(dLon / 2) * sin(dLon / 2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
return earthRadius * c
}
/**
* Stop location updates
*/
fun stopLocationUpdates(callback: LocationCallback) {
fusedLocationClient.removeLocationUpdates(callback)
}
}

View File

@ -1,52 +1,394 @@
package id.ac.ubharajaya.panicbutton
import android.Manifest
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import id.ac.ubharajaya.panicbutton.ui.theme.PanicButtonTheme
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
// ----------------------------------------------------------------------
// MAIN ACTIVITY CLASS
// ----------------------------------------------------------------------
class MainActivity : ComponentActivity() {
private lateinit var locationManager: LocationManager
// Permission launcher
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
// Permission result akan di-handle di composable
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
locationManager = LocationManager(this)
setContent {
PanicButtonTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
MyApp(
locationManager = locationManager,
onRequestPermission = {
requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
)
}
}
}
}
}
// ----------------------------------------------------------------------
// MY APP (NAVIGATOR)
// ----------------------------------------------------------------------
sealed class ScreenState {
object Main : ScreenState()
object Settings : ScreenState()
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
fun MyApp(
locationManager: LocationManager,
onRequestPermission: () -> Unit
) {
var currentScreen by remember { mutableStateOf<ScreenState>(ScreenState.Main) }
val context = LocalContext.current
Scaffold(
topBar = {
TopAppBar(
title = { Text("Panic Button") },
actions = {
if (currentScreen is ScreenState.Main) {
IconButton(onClick = {
currentScreen = ScreenState.Settings
}) {
Icon(Icons.Filled.Settings, contentDescription = "Settings")
}
}
}
)
}
) { paddingValues ->
when (currentScreen) {
is ScreenState.Main -> MainScreen(
paddingValues = paddingValues,
locationManager = locationManager,
onRequestPermission = onRequestPermission,
onSendNotification = { conditions, report, onResult ->
sendNotification(context, conditions, report, onResult)
},
onNavigateEvakuasi = {
val intent = Intent(context, JalurEvakuasiActivity::class.java)
context.startActivity(intent)
}
)
is ScreenState.Settings -> SettingsScreen(
onBack = { currentScreen = ScreenState.Main }
)
}
}
}
// ----------------------------------------------------------------------
// MAIN SCREEN
// ----------------------------------------------------------------------
@Composable
fun MainScreen(
paddingValues: PaddingValues,
locationManager: LocationManager,
onRequestPermission: () -> Unit,
onSendNotification: (String, String, (String) -> Unit) -> Unit,
onNavigateEvakuasi: () -> Unit
) {
val focusManager = LocalFocusManager.current
var message by remember { mutableStateOf("Klik tombol untuk mengirim notifikasi") }
var selectedConditions by remember { mutableStateOf(mutableSetOf<String>()) }
var additionalNotes by remember { mutableStateOf(TextFieldValue("")) }
// Location states
var locationData by remember { mutableStateOf<LocationManager.LocationData?>(null) }
var locationStatus by remember { mutableStateOf("Mengecek lokasi...") }
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
// Monitor location real-time
LaunchedEffect(Unit) {
if (locationManager.hasLocationPermission()) {
scope.launch {
locationManager.getLocationUpdates().collect { data ->
locationData = data
locationStatus = if (data != null) {
if (data.isInsideCampus) {
"📍 Anda berada di DALAM kampus Ubhara Jaya"
} else {
"📍 Anda berada di LUAR kampus Ubhara Jaya (${String.format("%.0f", data.distanceFromCampus)}m dari kampus)"
}
} else {
"❌ Tidak dapat mendapatkan lokasi"
}
}
}
} else {
locationStatus = "⚠️ Izin lokasi belum diberikan"
}
}
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start
) {
// LOCATION STATUS CARD
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
colors = CardDefaults.cardColors(
containerColor = if (locationData?.isInsideCampus == true)
Color(0xFF4CAF50).copy(alpha = 0.1f)
else
Color(0xFFFF9800).copy(alpha = 0.1f)
),
shape = RoundedCornerShape(8.dp)
) {
Column(
modifier = Modifier.padding(12.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 4.dp)
) {
Icon(
imageVector = Icons.Filled.LocationOn,
contentDescription = "Location",
tint = if (locationData?.isInsideCampus == true)
Color(0xFF4CAF50)
else
Color(0xFFFF9800),
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Hello $name!",
modifier = modifier
text = "Status Lokasi",
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
PanicButtonTheme {
Greeting("Android")
Text(
text = locationStatus,
fontSize = 13.sp,
color = Color.DarkGray
)
if (locationData != null) {
Text(
text = "Koordinat: ${String.format("%.6f", locationData!!.latitude)}, ${String.format("%.6f", locationData!!.longitude)}",
fontSize = 11.sp,
color = Color.Gray,
modifier = Modifier.padding(top = 4.dp)
)
}
// Tombol request permission jika belum diberikan
if (!locationManager.hasLocationPermission()) {
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onRequestPermission,
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF2196F3)
),
modifier = Modifier.fillMaxWidth()
) {
Text("Berikan Izin Lokasi", fontSize = 12.sp)
}
}
}
}
// EMERGENCY CONDITIONS SECTION
Text(
text = "Terjadi Kondisi Darurat",
fontSize = 20.sp,
color = Color.Red,
modifier = Modifier.padding(bottom = 16.dp)
)
listOf(
"🔥 Kebakaran", "⛈️ Banjir", "🌊 Tsunami", "🌋 Gunung Meletus",
"🌏 Gempa Bumi", "👿 Huru hara", "🐍 Binatang Buas",
"☢️ Radiasi Nuklir", "☣️ Biohazard"
).forEach { condition ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
focusManager.clearFocus()
selectedConditions = selectedConditions.toMutableSet().apply {
if (contains(condition)) remove(condition) else add(condition)
}
}
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = selectedConditions.contains(condition),
onCheckedChange = { isChecked ->
focusManager.clearFocus()
selectedConditions = selectedConditions.toMutableSet().apply {
if (isChecked) add(condition) else remove(condition)
}
}
)
Text(
text = condition,
modifier = Modifier.padding(start = 8.dp)
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(text = "Catatan tambahan:")
BasicTextField(
value = additionalNotes,
onValueChange = { additionalNotes = it },
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.padding(8.dp)
.border(1.dp, Color.Gray),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
val notes = additionalNotes.text
val conditions = selectedConditions.joinToString(", ")
val locationInfo = locationData?.let {
"\nLokasi: ${if (it.isInsideCampus) "Di dalam kampus" else "Di luar kampus"} (${String.format("%.6f", it.latitude)}, ${String.format("%.6f", it.longitude)})"
} ?: "\nLokasi: Tidak tersedia"
val report = "Kondisi: $conditions\nCatatan: $notes$locationInfo"
onSendNotification(conditions, report) { response ->
message = response
}
},
colors = ButtonDefaults.buttonColors(containerColor = Color.Red),
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Kirim Laporan", color = Color.White)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "JANGAN PANIK! SEGERA EVAKUASI\nDIRI ANDA KE TITIK KUMPUL",
color = Color.Red,
fontSize = 15.sp
)
Spacer(modifier = Modifier.height(16.dp))
Text(text = message, Modifier.padding(top = 16.dp))
Button(
onClick = onNavigateEvakuasi,
colors = ButtonDefaults.buttonColors(containerColor = Color.Green),
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Lihat Jalur Evakuasi", color = Color.White)
}
}
}
// ----------------------------------------------------------------------
// SEND NOTIFICATION FUNCTION
// ----------------------------------------------------------------------
fun sendNotification(context: Context, condition: String, report: String, onResult: (String) -> Unit) {
val client = OkHttpClient()
val url = getNtfyUrl(context)
val tagMapping = mapOf(
"🔥 Kebakaran" to "fire",
"⛈️ Banjir" to "cloud_with_lightning_and_rain",
"🌊 Tsunami" to "ocean",
"🌋 Gunung Meletus" to "volcano",
"🌏 Gempa Bumi" to "earth_asia",
"👿 Huru hara" to "imp",
"🐍 Binatang Buas" to "snake",
"☢️ Radiasi Nuklir" to "radioactive",
"☣️ Biohazard" to "biohazard"
)
val selectedList = condition
.split(",")
.map { it.trim() }
.filter { it.isNotEmpty() }
val cleanConditionText = selectedList.joinToString(", ")
val emojiTags = selectedList.mapNotNull { tagMapping[it] }
val finalTags = listOf("Alert") + emojiTags
val finalReport = report
val requestBody = RequestBody.create(
"text/plain".toMediaType(),
finalReport
)
val request = Request.Builder()
.url(url)
.addHeader("Title", "Alert")
.addHeader("Priority", "urgent")
.addHeader("Tags", finalTags.joinToString(","))
.post(requestBody)
.build()
Thread {
try {
val response = client.newCall(request).execute()
if (response.isSuccessful) {
onResult("Notifikasi berhasil dikirim ke $url!")
} else {
onResult("Gagal Mengirim notifikasi ke $url: ${response.code}")
}
} catch (e: Exception) {
onResult("Error: ${e.message}")
}
}.start()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

View File

@ -8,6 +8,7 @@ espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.9.4"
activityCompose = "1.11.0"
composeBom = "2024.09.00"
foundation = "1.9.4"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -24,6 +25,7 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }