feat: add AlertActivity, integrate OkHttp AlertSender, add network tests
This commit is contained in:
parent
75edbfa78f
commit
29603b706b
@ -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>
|
||||
150
app/src/main/java/id/ac/ubharajaya/panicbutton/AlertActivity.kt
Normal file
150
app/src/main/java/id/ac/ubharajaya/panicbutton/AlertActivity.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user