From 29603b706b2ca421834fec7f04c10fe97dc07f22 Mon Sep 17 00:00:00 2001 From: Rakha adi Date: Sat, 15 Nov 2025 22:36:40 +0700 Subject: [PATCH] feat: add AlertActivity, integrate OkHttp AlertSender, add network tests --- app/src/main/AndroidManifest.xml | 5 + .../ubharajaya/panicbutton/AlertActivity.kt | 150 ++++++++++++++++++ .../panicbutton/data/AlertSender.kt | 3 +- .../data/AlertSenderNetworkTest.kt | 61 +++++++ 4 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/id/ac/ubharajaya/panicbutton/AlertActivity.kt create mode 100644 app/src/test/java/id/ac/ubharajaya/panicbutton/data/AlertSenderNetworkTest.kt 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) + } +} +