feat: add AlertActivity, integrate OkHttp AlertSender, add network tests

This commit is contained in:
Rakha adi 2025-11-15 22:36:40 +07:00
parent 75edbfa78f
commit 29603b706b
4 changed files with 218 additions and 1 deletions

View File

@ -25,6 +25,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".AlertActivity"
android:exported="false"
android:label="Alert"
android:theme="@style/Theme.PanicButton" />
</application>
</manifest>

View File

@ -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>, 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<Int, Boolean>() }
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")
}
}
}

View File

@ -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.

View File

@ -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)
}
}