Compare commits

..

39 Commits

Author SHA1 Message Date
f289d0495f Delete SwiftVersion 2025-11-27 15:40:52 +07:00
7c7b9c0ec6 Add SwiftVersion 2025-11-27 15:40:27 +07:00
373c8595d7 Update README.md 2025-11-27 15:13:23 +07:00
d6dafed995 Update README.md 2025-11-13 20:19:16 +07:00
b42d846e01 Update README.md 2025-11-13 20:19:00 +07:00
2638da3f05 kasdka 2025-11-13 14:53:43 +07:00
08c24bdfdd Updated 2025-11-13 14:48:04 +07:00
d3c1410a11 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2025-11-13 14:47:37 +07:00
1ce89ad197 Updated 2025-11-13 14:47:11 +07:00
228d9f8f77 Hadi P. 2025-11-13 14:46:11 +07:00
5dffb47576 Hadi P 2025-11-13 14:45:15 +07:00
992c0d627c Hadi 2025-11-13 14:44:54 +07:00
4f1f8577bc Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2025-11-13 14:44:26 +07:00
6370eba0da Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2025-11-13 14:43:41 +07:00
c4aee9ee3c a. 2025-11-13 14:43:25 +07:00
5cccdf5368 a. 2025-11-13 14:42:12 +07:00
b73d429200 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2025-11-13 14:42:08 +07:00
89c60eb81e Hadi 2025-11-13 14:41:29 +07:00
6fcbb52b90 askd.., 2025-11-13 14:40:56 +07:00
cd156ca010 kasdka 2025-11-13 14:39:02 +07:00
4488dfd0cb Hadi 2025-11-13 14:38:11 +07:00
4281e24379 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2025-11-13 14:37:45 +07:00
5170efe60e askd.. 2025-11-13 14:37:17 +07:00
4ac1678d48 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2025-11-13 14:37:00 +07:00
357a25744c Hadi 2025-11-13 14:36:29 +07:00
cf5adbd923 askd. 2025-11-13 14:36:18 +07:00
d77a9409de Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2025-11-13 14:35:21 +07:00
a115da0783 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2025-11-13 14:35:01 +07:00
8f9ca68257 Merge remote-tracking branch 'origin/master' 2025-11-13 14:34:53 +07:00
83fabde258 askd 2025-11-13 14:34:41 +07:00
8f24ef2640 Merge remote-tracking branch 'origin/master' 2025-11-13 14:34:33 +07:00
3e3da371dc Hadi 2025-11-13 14:34:28 +07:00
6d30bb4a88 nama 2025-11-13 14:33:21 +07:00
wafi14-art
ae1ae8cd80 Fadhlul Wafi 2025-11-13 14:33:21 +07:00
1f7adae6f4 Updatee 2025-11-13 14:30:24 +07:00
038c8acf5d Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2025-11-13 14:29:39 +07:00
55d17abb93 Praktikum 1 2025-11-13 14:29:32 +07:00
49484644c0 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2025-11-13 14:27:28 +07:00
2ef406d999 Update 2025-11-13 14:27:04 +07:00
10 changed files with 65 additions and 885 deletions

View File

@ -1,10 +0,0 @@
<?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,21 +9,28 @@ Repository ini digunakan untuk praktikum perkuliahan pemrograman mobile.
- Arif Dwiyanto: arif.dwiyanto@ubharajaya.ac.id
## Mahasiswa
- 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)
- 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)

View File

@ -6,7 +6,9 @@ plugins {
android {
namespace = "id.ac.ubharajaya.panicbutton"
compileSdk = 36
compileSdk {
version = release(36)
}
defaultConfig {
applicationId = "id.ac.ubharajaya.panicbutton"
@ -40,33 +42,14 @@ 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.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)
implementation(libs.androidx.compose.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@ -74,4 +57,8 @@ 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,13 +2,6 @@
<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"
@ -17,28 +10,18 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PanicButton"
android:usesCleartextTraffic="true"
>
android:theme="@style/Theme.PanicButton">
<activity
android:name="id.ac.ubharajaya.panicbutton.MainActivity"
android:name=".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

@ -1,167 +0,0 @@
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

@ -1,62 +0,0 @@
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

@ -1,214 +0,0 @@
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,394 +1,52 @@
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.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.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.compose.ui.Modifier
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 androidx.compose.ui.tooling.preview.Preview
import id.ac.ubharajaya.panicbutton.ui.theme.PanicButtonTheme
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)
locationManager = LocationManager(this)
enableEdgeToEdge()
setContent {
MyApp(
locationManager = locationManager,
onRequestPermission = {
requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
PanicButtonTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
// ----------------------------------------------------------------------
// MY APP (NAVIGATOR)
// ----------------------------------------------------------------------
sealed class ScreenState {
object Main : ScreenState()
object Settings : ScreenState()
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
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))
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Status Lokasi",
fontSize = 14.sp,
fontWeight = FontWeight.Bold
text = "Hello $name!",
modifier = modifier
)
}
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}")
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
PanicButtonTheme {
Greeting("Android")
}
} catch (e: Exception) {
onResult("Error: ${e.message}")
}
}.start()
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

View File

@ -8,7 +8,6 @@ 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" }
@ -25,7 +24,6 @@ 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" }