diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2a30d75..5038c1f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -25,6 +25,11 @@
+
\ No newline at end of file
diff --git a/app/src/main/java/id/ac/ubharajaya/panicbutton/AlertActivity.kt b/app/src/main/java/id/ac/ubharajaya/panicbutton/AlertActivity.kt
new file mode 100644
index 0000000..52c7ff7
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/panicbutton/AlertActivity.kt
@@ -0,0 +1,150 @@
+package id.ac.ubharajaya.panicbutton
+
+import android.os.Bundle
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import id.ac.ubharajaya.panicbutton.data.AlertSender
+import id.ac.ubharajaya.panicbutton.ui.theme.PanicButtonTheme
+
+// New AlertActivity: shows big warning, emergency checkboxes, notes input and a send button
+class AlertActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ PanicButtonTheme {
+ Surface(modifier = Modifier.fillMaxSize()) {
+ AlertScreen(onSend = { selected, notes ->
+ // Build message from selected options and notes
+ val message = buildString {
+ if (selected.isNotEmpty()) append(selected.joinToString(", "))
+ if (notes.isNotBlank()) {
+ if (isNotEmpty()) append("\n")
+ append(notes)
+ }
+ }
+ // Call AlertSender
+ AlertSender.sendAlert(message) { result ->
+ runOnUiThread {
+ result.fold(onSuccess = { _ ->
+ Toast.makeText(this@AlertActivity, "Laporan terkirim", Toast.LENGTH_SHORT).show()
+ finish()
+ }, onFailure = { err ->
+ Toast.makeText(this@AlertActivity, "Gagal mengirim: ${err.message}", Toast.LENGTH_LONG).show()
+ })
+ }
+ }
+ })
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun AlertScreen(onSend: (List, String) -> Unit) {
+ val options = listOf(
+ "Kebakaran",
+ "Banjir",
+ "Gempa",
+ "Bumi",
+ "Huru-hara/Demonstrasi"
+ )
+
+ // store checked states in a map so state survives recomposition
+ val checkedStates = rememberSaveable { mutableStateMapOf() }
+ for (i in options.indices) {
+ if (!checkedStates.containsKey(i)) checkedStates[i] = false
+ }
+
+ var notes by rememberSaveable { mutableStateOf("") }
+ var isSending by remember { mutableStateOf(false) }
+ val context = LocalContext.current
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp)
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text(
+ text = "JANGAN PANIK! SEGERA EVAKUASI DIRI ANDA KE TITIK KUMPUL",
+ color = MaterialTheme.colorScheme.error,
+ fontSize = 20.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+
+ Text(text = "Pilih kondisi darurat:", fontWeight = FontWeight.SemiBold)
+
+ options.forEachIndexed { idx, label ->
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Checkbox(
+ checked = checkedStates[idx] == true,
+ onCheckedChange = { checkedStates[idx] = it }
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = label)
+ }
+ }
+
+ OutlinedTextField(
+ value = notes,
+ onValueChange = { notes = it },
+ label = { Text("Catatan tambahan peristiwa") },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(120.dp)
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ if (isSending) {
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
+ CircularProgressIndicator()
+ }
+ }
+
+ Button(
+ onClick = {
+ val selected = options.mapIndexedNotNull { idx, label ->
+ if (checkedStates[idx] == true) label else null
+ }
+ if (selected.isEmpty() && notes.isBlank()) {
+ Toast.makeText(context, "Pilih minimal satu kondisi atau isi catatan", Toast.LENGTH_SHORT).show()
+ return@Button
+ }
+ isSending = true
+ onSend(selected, notes)
+ // isSending will be reset by the activity's callback via recomposition if needed
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp)
+ ) {
+ Text(text = "Kirim Laporan")
+ }
+ }
+}
diff --git a/app/src/main/java/id/ac/ubharajaya/panicbutton/data/AlertSender.kt b/app/src/main/java/id/ac/ubharajaya/panicbutton/data/AlertSender.kt
index 81c119d..8bf34dd 100644
--- a/app/src/main/java/id/ac/ubharajaya/panicbutton/data/AlertSender.kt
+++ b/app/src/main/java/id/ac/ubharajaya/panicbutton/data/AlertSender.kt
@@ -12,7 +12,8 @@ import java.io.IOException
object AlertSender {
// Make endpoint mutable so tests can override it
var ENDPOINT: String = "https://ntfy.ubharajaya.ac.id/panic-button"
- private val client = OkHttpClient()
+ // Allow client to be replaced in tests
+ var client: OkHttpClient = OkHttpClient()
/**
* Send an alert message to the configured endpoint asynchronously using OkHttp.
diff --git a/app/src/test/java/id/ac/ubharajaya/panicbutton/data/AlertSenderNetworkTest.kt b/app/src/test/java/id/ac/ubharajaya/panicbutton/data/AlertSenderNetworkTest.kt
new file mode 100644
index 0000000..a7eae77
--- /dev/null
+++ b/app/src/test/java/id/ac/ubharajaya/panicbutton/data/AlertSenderNetworkTest.kt
@@ -0,0 +1,61 @@
+package id.ac.ubharajaya.panicbutton.data
+
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+class AlertSenderNetworkTest {
+ private lateinit var server: MockWebServer
+
+ @Before
+ fun setup() {
+ server = MockWebServer()
+ server.start()
+ // point AlertSender to the mock server
+ AlertSender.ENDPOINT = server.url("/panic-button").toString()
+ }
+
+ @After
+ fun tearDown() {
+ server.shutdown()
+ }
+
+ @Test
+ fun `sendAlert success returns 200`() {
+ val latch = CountDownLatch(1)
+ server.enqueue(MockResponse().setResponseCode(200))
+
+ var resultCode: Int? = null
+ AlertSender.sendAlert("test message") { res ->
+ res.fold(onSuccess = { code -> resultCode = code }, onFailure = { /* ignore */ })
+ latch.countDown()
+ }
+
+ val awaited = latch.await(5, TimeUnit.SECONDS)
+ assertTrue("Request didn't complete in time", awaited)
+ assertEquals(200, resultCode)
+ }
+
+ @Test
+ fun `sendAlert failure returns non-2xx`() {
+ val latch = CountDownLatch(1)
+ server.enqueue(MockResponse().setResponseCode(500))
+
+ var errMsg: String? = null
+ AlertSender.sendAlert("test message") { res ->
+ res.fold(onSuccess = { /* ignore */ }, onFailure = { err -> errMsg = err.message })
+ latch.countDown()
+ }
+
+ val awaited = latch.await(5, TimeUnit.SECONDS)
+ assertTrue("Request didn't complete in time", awaited)
+ assertTrue(errMsg?.contains("HTTP 500") == true)
+ }
+}
+