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" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".AlertActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="Alert"
|
||||||
|
android:theme="@style/Theme.PanicButton" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</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 {
|
object AlertSender {
|
||||||
// Make endpoint mutable so tests can override it
|
// Make endpoint mutable so tests can override it
|
||||||
var ENDPOINT: String = "https://ntfy.ubharajaya.ac.id/panic-button"
|
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.
|
* 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