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:
parent
b0531c0412
commit
56ec2b4263
@ -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")
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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 ->
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user