feat: sertakan koordinat GPS pada alert; tambahkan permission lokasi dan FusedLocationProvider; pisahkan server NTFY dan topic; perbarui MainActivity, MainViewModel, NotificationSender, MainScreen

This commit is contained in:
rakha 2025-11-27 13:46:23 +07:00
parent b0531c0412
commit 56ec2b4263
4 changed files with 61 additions and 4 deletions

View File

@ -69,4 +69,7 @@ dependencies {
// AndroidX SplashScreen (required for installSplashScreen API) // AndroidX SplashScreen (required for installSplashScreen API)
implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.core:core-splashscreen:1.0.1")
// Play Services Location for GPS (FusedLocationProviderClient)
implementation("com.google.android.gms:play-services-location:21.0.1")
} }

View File

@ -1,21 +1,68 @@
package id.ac.ubharajaya.panicbutton package id.ac.ubharajaya.panicbutton
import android.Manifest
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import com.google.android.gms.location.LocationServices
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var viewModel: MainViewModel private lateinit var viewModel: MainViewModel
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
// result handled inline when requesting
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java) viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
val fusedClient = LocationServices.getFusedLocationProviderClient(this)
val openEvacMaps = { val openEvacMaps = {
startActivity(Intent(this, EvacuationMapsActivity::class.java)) startActivity(Intent(this, EvacuationMapsActivity::class.java))
} }
// onSendAlert will try to get location (if permission granted) and then call viewModel.sendAlert
val onSendAlert = {
// Check permission
val fineGranted = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
val coarseGranted = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
if (!fineGranted && !coarseGranted) {
// Request fine location permission (preferred)
requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
// After permission flow, user will need to press the button again to actually send with location
viewModel.dialogMessage = "Izin lokasi dibutuhkan untuk menyertakan koordinat. Silakan tekan lagi setelah mengizinkan lokasi."
return@let
}
// Try get last location asynchronously
CoroutineScope(Dispatchers.Main).launch {
try {
val loc = fusedClient.lastLocation.await() // need extension await - we'll handle fallback
if (loc != null) {
viewModel.sendAlert(loc.latitude, loc.longitude)
} else {
// fallback: send without coordinates
viewModel.sendAlert()
}
} catch (e: Exception) {
viewModel.sendAlert()
}
}
}
setContent { setContent {
MaterialTheme { MaterialTheme {
MainScreen(viewModel = viewModel, onOpenEvacMaps = openEvacMaps) MainScreen(viewModel = viewModel, onOpenEvacMaps = openEvacMaps)

View File

@ -12,7 +12,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.foundation.shape.RoundedCornerShape
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -28,7 +28,7 @@ import androidx.compose.foundation.layout.IntrinsicSize
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit) { fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit, onSendAlert: () -> Unit) {
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomBar = { bottomBar = {
@ -38,7 +38,7 @@ fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit) {
.height(120.dp), .height(120.dp),
contentAlignment = Alignment.TopCenter contentAlignment = Alignment.TopCenter
) { ) {
PanicButton(onClick = { viewModel.sendAlert() }, buttonSize = 130.dp, shadowSize = 160.dp) PanicButton(onClick = { onSendAlert() }, buttonSize = 130.dp, shadowSize = 160.dp)
} }
} }
) { padding -> ) { padding ->

View File

@ -40,7 +40,8 @@ class MainViewModel : ViewModel() {
} }
} }
fun sendAlert() { // latitude/longitude optional parameters
fun sendAlert(latitude: Double? = null, longitude: Double? = null) {
val selectedOptions = options.filter { isChecked(it.label) }.map { it.label } val selectedOptions = options.filter { isChecked(it.label) }.map { it.label }
if (selectedOptions.isEmpty()) { if (selectedOptions.isEmpty()) {
@ -59,6 +60,12 @@ class MainViewModel : ViewModel() {
if (otherNote.isNotBlank()) { if (otherNote.isNotBlank()) {
bodyBuilder.append("\nCatatan: ${otherNote.trim()}") bodyBuilder.append("\nCatatan: ${otherNote.trim()}")
} }
// Append coordinates if available
if (latitude != null && longitude != null) {
bodyBuilder.append("\nLokasi: https://maps.google.com/?q=$latitude,$longitude")
}
val payload = bodyBuilder.toString() val payload = bodyBuilder.toString()
// Use NotificationSender utility to send the payload // Use NotificationSender utility to send the payload