Compare commits

..

No commits in common. "835b856792ffe6cdb8d80c4e01afb4c64f9c7e94" and "8a0bef9d0dcb71d706317ba74807eafa8f2cea9a" have entirely different histories.

3 changed files with 350 additions and 120 deletions

View File

@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

View File

@ -3,23 +3,15 @@ package id.ac.ubharajaya.panicbutton
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll 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.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
@ -27,141 +19,245 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody 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() { class AlertActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
SettingsScreen(onBack = { finish() }) AlertScreen()
} }
} }
} }
// ----------------------------------------------------------------------
// SETTINGS SCREEN
// ----------------------------------------------------------------------
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen(onBack: () -> Unit) { fun AlertScreen() {
val context = LocalContext.current val context = androidx.compose.ui.platform.LocalContext.current
val prefs = remember { getPrefs(context) } val sharedPreferences = context.getSharedPreferences("PanicButtonPrefs", Context.MODE_PRIVATE)
// Inisialisasi state dari SharedPreferences // Ambil konfigurasi dari SharedPreferences
var serverText by remember { val ntfyUrl = remember {
mutableStateOf(TextFieldValue(prefs.getString(KEY_NTFY_SERVER, DEFAULT_NTFY_SERVER) ?: DEFAULT_NTFY_SERVER)) sharedPreferences.getString("urlKey", null)
} ?: "${sharedPreferences.getString("ntfyServer", "https://ntfy.sh")}/${sharedPreferences.getString("ntfyTopic", "ubahara-panic-button")}"
var topicText by remember {
mutableStateOf(TextFieldValue(prefs.getString(KEY_NTFY_TOPIC, DEFAULT_NTFY_TOPIC) ?: DEFAULT_NTFY_TOPIC))
} }
var saveMessage by remember { mutableStateOf("") } var alertMessage by remember { mutableStateOf("") }
var showDialog by remember { mutableStateOf(false) }
var dialogMessage by remember { mutableStateOf("") }
var selectedAlertType by remember { mutableStateOf("") }
// ⬅️ TAMBAHAN: Menangani tombol kembali pada perangkat val scrollState = rememberScrollState()
BackHandler(onBack = onBack)
Scaffold( // Dialog konfirmasi
topBar = { if (showDialog) {
TopAppBar( AlertDialog(
title = { Text("Pengaturan NTFY Endpoint") }, onDismissRequest = { showDialog = false },
navigationIcon = { title = { Text("Status Pengiriman") },
IconButton(onClick = onBack) { // ⬅️ onBack menangani navigasi kembali ke MainScreen text = { Text(dialogMessage) },
Icon(Icons.Filled.ArrowBack, contentDescription = "Kembali") confirmButton = {
} Button(onClick = { showDialog = false }) {
Text("OK")
} }
) }
} )
) { 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( Column(
value = serverText, modifier = Modifier
onValueChange = { serverText = it }, .fillMaxSize()
label = { Text("Server NTFY (contoh: https://ntfy.sh)") }, .verticalScroll(scrollState)
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), .padding(16.dp),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), verticalArrangement = Arrangement.Top,
singleLine = true horizontalAlignment = Alignment.CenterHorizontally
) ) {
Text(
text = "⚠️ SISTEM ALERT DARURAT",
fontSize = 24.sp,
color = Color.Red,
modifier = Modifier.padding(bottom = 8.dp)
)
OutlinedTextField( Text(
value = topicText, text = "Endpoint: $ntfyUrl",
onValueChange = { topicText = it }, fontSize = 12.sp,
label = { Text("Topic (contoh: panic-button)") }, color = Color.Gray,
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), modifier = Modifier.padding(bottom = 24.dp)
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), )
singleLine = true
)
// Tombol Alert Cepat
val alertTypes = listOf(
"🔥 KEBAKARAN" to "fire",
"🌊 BANJIR/TSUNAMI" to "ocean",
"🌏 GEMPA BUMI" to "earth_asia",
"☢️ BAHAYA RADIASI" to "radioactive",
"🚨 EVAKUASI SEGERA" to "rotating_light"
)
alertTypes.forEach { (label, tag) ->
Button( Button(
onClick = { onClick = {
saveNtfyConfig( selectedAlertType = label
context, sendQuickAlert(ntfyUrl, label, tag) { response ->
serverText.text.trim(), alertMessage = response
topicText.text.trim() dialogMessage = response
) showDialog = true
saveMessage = "Konfigurasi berhasil disimpan! URL: ${getNtfyUrl(context)}" }
}, },
modifier = Modifier.fillMaxWidth() colors = ButtonDefaults.buttonColors(
containerColor = Color.Red
),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) { ) {
Text("Simpan Konfigurasi") Text(
text = label,
color = Color.White,
fontSize = 18.sp,
modifier = Modifier.padding(vertical = 8.dp)
)
} }
}
Spacer(modifier = Modifier.height(24.dp))
// Custom Alert dengan input
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color(0xFFFFF3E0))
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "ALERT KUSTOM",
fontSize = 16.sp,
color = Color.Red
)
Spacer(modifier = Modifier.height(8.dp))
var customMessage by remember { mutableStateOf("") }
OutlinedTextField(
value = customMessage,
onValueChange = { customMessage = it },
label = { Text("Pesan Alert") },
modifier = Modifier.fillMaxWidth(),
minLines = 3
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
if (customMessage.isNotEmpty()) {
sendCustomAlert(ntfyUrl, customMessage) { response ->
alertMessage = response
dialogMessage = response
showDialog = true
customMessage = ""
}
}
},
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFF6F00)),
modifier = Modifier.fillMaxWidth()
) {
Text("Kirim Alert Kustom")
}
}
}
Spacer(modifier = Modifier.height(16.dp))
if (alertMessage.isNotEmpty()) {
Text( Text(
text = saveMessage, text = alertMessage,
color = if (saveMessage.contains("berhasil")) Color.Green else Color.Black, color = if (alertMessage.contains("berhasil")) Color.Green else Color.Red,
modifier = Modifier.padding(top = 16.dp) 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)
)
} }
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "TETAP TENANG DAN IKUTI PROSEDUR EVAKUASI",
color = Color.Red,
fontSize = 14.sp
)
} }
}
fun sendQuickAlert(url: String, alertType: String, tag: String, onResult: (String) -> Unit) {
val client = OkHttpClient()
val message = """
ALERT DARURAT
Jenis: $alertType
Waktu: ${java.text.SimpleDateFormat("dd/MM/yyyy HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())}
SEGERA LAKUKAN EVAKUASI!
""".trimIndent()
val requestBody = RequestBody.create(
"text/plain".toMediaType(),
message
)
val request = Request.Builder()
.url(url)
.addHeader("Title", "🚨 ALERT DARURAT - $alertType")
.addHeader("Priority", "urgent")
.addHeader("Tags", "warning,alert,$tag,rotating_light")
.post(requestBody)
.build()
Thread {
try {
val response = client.newCall(request).execute()
if (response.isSuccessful) {
onResult("✅ Alert $alertType berhasil dikirim!")
} else {
onResult("❌ Gagal mengirim alert: ${response.code}")
}
} catch (e: Exception) {
onResult("❌ Error: ${e.message}")
}
}.start()
}
fun sendCustomAlert(url: String, customMessage: String, onResult: (String) -> Unit) {
val client = OkHttpClient()
val message = """
📢 PESAN DARURAT KUSTOM
$customMessage
Waktu: ${java.text.SimpleDateFormat("dd/MM/yyyy HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())}
""".trimIndent()
val requestBody = RequestBody.create(
"text/plain".toMediaType(),
message
)
val request = Request.Builder()
.url(url)
.addHeader("Title", "📢 Pesan Darurat Kustom")
.addHeader("Priority", "high")
.addHeader("Tags", "warning,alert,loudspeaker")
.post(requestBody)
.build()
Thread {
try {
val response = client.newCall(request).execute()
if (response.isSuccessful) {
onResult("✅ Pesan kustom berhasil dikirim!")
} else {
onResult("❌ Gagal mengirim pesan: ${response.code}")
}
} catch (e: Exception) {
onResult("❌ Error: ${e.message}")
}
}.start()
} }

View File

@ -34,7 +34,40 @@ import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// MAIN ACTIVITY CLASS // 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"
}
// ----------------------------------------------------------------------
// 2. MAIN ACTIVITY CLASS
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -47,7 +80,7 @@ class MainActivity : ComponentActivity() {
} }
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// MY APP (NAVIGATOR) // 3. MY APP (NAVIGATOR)
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
sealed class ScreenState { sealed class ScreenState {
@ -100,7 +133,7 @@ fun MyApp() {
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// MAIN SCREEN // 4. MAIN SCREEN
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@Composable @Composable
@ -213,7 +246,104 @@ fun MainScreen(
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// SEND NOTIFICATION FUNCTION // 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) { fun sendNotification(context: Context, condition: String, report: String, onResult: (String) -> Unit) {