From e2bd34be8a040c91c7b1e01b9b2da76eb6bacbba Mon Sep 17 00:00:00 2001
From: dend <2023010715051@mhs.ubharajaya.ac.id>
Date: Fri, 28 Nov 2025 00:26:58 +0700
Subject: [PATCH] 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.
---
app/build.gradle.kts | 3 +
app/src/main/AndroidManifest.xml | 1 +
.../ac/ubharajaya/panicbutton/MainActivity.kt | 363 +++++++++++-------
3 files changed, 236 insertions(+), 131 deletions(-)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index c53de8a..f685a40 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -63,5 +63,8 @@ dependencies {
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("androidx.compose.material3:material3:1.1.1")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a12cf55..ca2c22b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
+
(ScreenState.Main) }
val context = LocalContext.current
- val sharedPreferences = context.getSharedPreferences("PanicButtonPrefs", Context.MODE_PRIVATE)
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Panic Button") },
+ // Logika TopBar hanya muncul jika di MainScreen,
+ // SettingsScreen memiliki TopBar sendiri
+ 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,
+ onSendNotification = { conditions, report, onResult ->
+ sendNotification(context, conditions, report, onResult)
+ },
+ onNavigateEvakuasi = {
+ val intent = Intent(context, JalurEvakuasiActivity::class.java)
+ context.startActivity(intent)
+ }
+ )
+ is ScreenState.Settings -> SettingsScreen(
+ // Melewatkan fungsi untuk kembali
+ onBack = { currentScreen = ScreenState.Main }
+ )
+ }
+ }
+}
+
+
+// ----------------------------------------------------------------------
+// 4. MAIN SCREEN
+// ----------------------------------------------------------------------
+
+@Composable
+fun MainScreen(
+ paddingValues: PaddingValues,
+ 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()) }
var additionalNotes by remember { mutableStateOf(TextFieldValue("")) }
- var showDialog by remember { mutableStateOf(false) }
- var dialogMessage by remember { mutableStateOf("") }
- var showSettingsDialog by remember { mutableStateOf(false) }
-
- // Load saved configuration
- var ntfyServer by remember {
- mutableStateOf(sharedPreferences.getString("ntfyServer", "https://ntfy.sh") ?: "https://ntfy.sh")
- }
- var ntfyTopic by remember {
- mutableStateOf(sharedPreferences.getString("ntfyTopic", "ubahara-panic-button") ?: "ubahara-panic-button")
- }
val scrollState = rememberScrollState()
- // Dialog konfirmasi
- if (showDialog) {
- AlertDialog(
- onDismissRequest = { showDialog = false },
- title = { Text("Konfirmasi") },
- text = { Text(dialogMessage) },
- confirmButton = {
- Button(onClick = { showDialog = false }) {
- Text("OK")
- }
- }
- )
- }
-
- // Dialog Settings
- if (showSettingsDialog) {
- var tempServer by remember { mutableStateOf(ntfyServer) }
- var tempTopic by remember { mutableStateOf(ntfyTopic) }
-
- AlertDialog(
- onDismissRequest = { showSettingsDialog = false },
- title = { Text("Pengaturan NTFY") },
- text = {
- Column {
- Text("Server NTFY:")
- OutlinedTextField(
- value = tempServer,
- onValueChange = { tempServer = it },
- modifier = Modifier.fillMaxWidth()
- )
- Spacer(modifier = Modifier.height(8.dp))
- Text("Topic:")
- OutlinedTextField(
- value = tempTopic,
- onValueChange = { tempTopic = it },
- modifier = Modifier.fillMaxWidth()
- )
- }
- },
- confirmButton = {
- Button(onClick = {
- ntfyServer = tempServer
- ntfyTopic = tempTopic
- // Save to SharedPreferences
- with(sharedPreferences.edit()) {
- putString("ntfyServer", tempServer)
- putString("ntfyTopic", tempTopic)
- // Gabungkan untuk kompatibilitas dengan AlertActivity
- putString("urlKey", "$tempServer/$tempTopic")
- apply()
- }
- showSettingsDialog = false
- message = "Pengaturan berhasil disimpan"
- }) {
- Text("Simpan")
- }
- },
- dismissButton = {
- TextButton(onClick = { showSettingsDialog = false }) {
- Text("Batal")
- }
- }
- )
- }
-
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
+ .padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start
) {
- // Header dengan tombol settings
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = "Terjadi Kondisi Darurat",
- fontSize = 20.sp,
- color = Color.Red
- )
- Button(
- onClick = { showSettingsDialog = true },
- colors = ButtonDefaults.buttonColors(containerColor = Color.Gray)
- ) {
- Text("βοΈ", fontSize = 18.sp)
- }
- }
-
- Spacer(modifier = Modifier.height(8.dp))
Text(
- text = "Server: $ntfyServer\nTopic: $ntfyTopic",
- fontSize = 12.sp,
- color = Color.Gray,
+ 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"
+ "\uD83D\uDD25 Kebakaran", "β\uFE0F Banjir", "\uD83C\uDF0A Tsunami", "\uD83C\uDF0B Gunung Meletus",
+ "\uD83C\uDF0F Gempa Bumi", "\uD83D\uDC7F Huru hara", "\uD83D\uDC0D Binatang Buas",
+ "β’\uFE0F Radiasi Nuklir", "β£\uFE0F Biohazard"
).forEach { condition ->
Row(
modifier = Modifier
@@ -212,23 +217,18 @@ fun MyApp() {
val notes = additionalNotes.text
val conditions = selectedConditions.joinToString(", ")
val report = "Kondisi: $conditions\nCatatan: $notes"
- val fullUrl = "$ntfyServer/$ntfyTopic"
-
- sendNotification(fullUrl, conditions, report) { response ->
+ onSendNotification(conditions, report) { response ->
message = response
- dialogMessage = response
- showDialog = true
}
},
- colors = ButtonDefaults.buttonColors(containerColor = Color.Red),
- enabled = selectedConditions.isNotEmpty()
+ colors = ButtonDefaults.buttonColors(containerColor = Color.Red)
) {
Text(text = "Kirim Laporan", color = Color.White)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
- text = "JANGAN PANIK! SEGERA EVAKUASI\nDIRI ANDA KE TITIK KUMPUL",
+ text = "βJANGAN PANIK! SEGERA EVAKUASI\nDIRI ANDA KE TITIK KUMPULβ",
color = Color.Red,
fontSize = 15.sp
)
@@ -236,10 +236,7 @@ fun MyApp() {
Spacer(modifier = Modifier.height(16.dp))
Text(text = message, Modifier.padding(top = 16.dp))
Button(
- onClick = {
- val intent = Intent(context, JalurEvakuasiActivity::class.java)
- context.startActivity(intent)
- },
+ onClick = onNavigateEvakuasi,
colors = ButtonDefaults.buttonColors(containerColor = Color.Green)
) {
Text(text = "Lihat Jalur Evakuasi", color = Color.White)
@@ -247,19 +244,122 @@ fun MyApp() {
}
}
-fun sendNotification(url: String, condition: String, report: String, onResult: (String) -> Unit) {
+
+// ----------------------------------------------------------------------
+// 5. SETTINGS SCREEN (DIPERBAIKI)
+// ----------------------------------------------------------------------
+
+@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)
+ )
+ }
+ }
+}
+
+
+// ----------------------------------------------------------------------
+// 6. 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"
+ "\uD83D\uDD25 Kebakaran" to "fire",
+ "β\uFE0F Banjir" to "cloud_with_lightning_and_rain",
+ "\uD83C\uDF0A Tsunami" to "ocean",
+ "\uD83C\uDF0B Gunung Meletus" to "volcano",
+ "\uD83C\uDF0F Gempa Bumi" to "earth_asia",
+ "\uD83D\uDC7F Huru hara" to "imp",
+ "\uD83D\uDC0D Binatang Buas" to "snake",
+ "β’\uFE0F Radiasi Nuklir" to "radioactive",
+ "β£\uFE0F Biohazard" to "biohazard"
)
val selectedList = condition
@@ -269,7 +369,8 @@ fun sendNotification(url: String, condition: String, report: String, onResult: (
val cleanConditionText = selectedList.joinToString(", ")
val emojiTags = selectedList.mapNotNull { tagMapping[it] }
- val finalTags = listOf("alert", "warning") + emojiTags
+
+ val finalTags = listOf("Alert") + emojiTags
val finalReport = "Kondisi: $cleanConditionText\nCatatan: ${report.substringAfter("Catatan:")}"
val requestBody = RequestBody.create(
@@ -278,7 +379,7 @@ fun sendNotification(url: String, condition: String, report: String, onResult: (
)
val request = Request.Builder()
.url(url)
- .addHeader("Title", "β οΈ ALERT - Kondisi Darurat")
+ .addHeader("Title", "Alert")
.addHeader("Priority", "urgent")
.addHeader("Tags", finalTags.joinToString(","))
.post(requestBody)
@@ -288,12 +389,12 @@ fun sendNotification(url: String, condition: String, report: String, onResult: (
try {
val response = client.newCall(request).execute()
if (response.isSuccessful) {
- onResult("β
Notifikasi berhasil dikirim!\nLaporan telah terkirim ke sistem.")
+ onResult("Notifikasi berhasil dikirim ke $url!")
} else {
- onResult("β Gagal mengirim notifikasi\nKode error: ${response.code}")
+ onResult("Gagal mengirim notifikasi ke $url: ${response.code}")
}
} catch (e: Exception) {
- onResult("β Error: ${e.message}")
+ onResult("Error: ${e.message}")
}
}.start()
}
\ No newline at end of file