feat: integrar autenticacion QR, sesion y mejoras M530

This commit is contained in:
Miguel Angel 2026-09-15 10:20:50 -05:00
parent 9deeb40217
commit 0d87c0e4d9
19 changed files with 2322 additions and 212 deletions

View file

@ -1,4 +1,4 @@
package com.bodycamera.twentyfoulabs
package com.bodycamera.twentyfoulabs
import android.Manifest
import android.content.BroadcastReceiver
@ -44,10 +44,128 @@ import androidx.hilt.navigation.compose.hiltViewModel
import javax.inject.Inject
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicText
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.collect
@Composable
private fun M530StartupSplash() {
val alpha = remember {
Animatable(0f)
}
val scale = remember {
Animatable(0.90f)
}
LaunchedEffect(Unit) {
launch {
alpha.animateTo(
targetValue = 1f,
animationSpec = tween(650)
)
}
launch {
scale.animateTo(
targetValue = 1f,
animationSpec = tween(750)
)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF071827)),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier
.alpha(alpha.value)
.graphicsLayer {
scaleX = scale.value
scaleY = scale.value
},
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
BasicText(
text = "GEOSATELITAL",
style = TextStyle(
color = Color.White,
fontSize = 28.sp,
fontWeight = FontWeight.Bold
)
)
Spacer(
modifier = Modifier.height(8.dp)
)
BasicText(
text = "M530 COMMAND",
style = TextStyle(
color = Color(0xFF00D7FF),
fontSize = 16.sp,
fontWeight = FontWeight.Medium
)
)
Spacer(
modifier = Modifier.height(28.dp)
)
Box(
modifier = Modifier
.width(100.dp)
.height(3.dp)
.background(Color(0xFF00D7FF))
)
Spacer(
modifier = Modifier.height(12.dp)
)
BasicText(
text = "INICIANDO...",
style = TextStyle(
color = Color(0xFFB7C6D3),
fontSize = 11.sp
)
)
}
}
}
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@ -63,6 +181,15 @@ class MainActivity : ComponentActivity() {
private const val TAG = "MainActivity"
private const val ACTION_VIDEO_DOWN = "android.intent.action.ACTION_VIDEO_DOWN"
private const val ACTION_VIDEO_UP = "android.intent.action.ACTION_VIDEO_UP"
private const val ACTION_PTT_DOWN =
"android.intent.action.ACTION_PTTKEY_DOWN"
private const val ACTION_PTT_UP =
"android.intent.action.ACTION_PTTKEY_UP"
private const val ACTION_PTT_LONG_PRESS =
"android.intent.action.ACTION_PTTKEY_LONG_PRESS"
}
private var isReceiverRegistered = false
@ -70,8 +197,51 @@ class MainActivity : ComponentActivity() {
private val hardwareReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
ACTION_VIDEO_DOWN -> handleVideoButton()
ACTION_VIDEO_UP -> Unit
ACTION_PTT_DOWN -> {
Log.i(TAG, "PTT BROADCAST DOWN recibido")
if (!isAuthenticated) {
toast("Inicia sesion para usar PTT")
return
}
val result = deviceController.startPtt()
Log.i(
TAG,
if (result.isSuccess)
"PTT BROADCAST DOWN - PTT iniciado"
else
"PTT BROADCAST DOWN - ERROR: ${result.exceptionOrNull()?.message}"
)
}
ACTION_PTT_LONG_PRESS -> {
Log.d(TAG, "PTT BROADCAST LONG PRESS ignorado")
}
ACTION_PTT_UP -> {
Log.i(TAG, "PTT BROADCAST UP recibido")
if (!isAuthenticated) {
return
}
val result = deviceController.stopPtt()
Log.i(
TAG,
if (result.isSuccess)
"PTT BROADCAST UP - PTT detenido"
else
"PTT BROADCAST UP - ERROR: ${result.exceptionOrNull()?.message}"
)
}
}
}
}
@ -114,7 +284,30 @@ class MainActivity : ComponentActivity() {
deviceController.logTelemetry()
setContent {
MainAppTheme { AppNavigation(viewModel = viewModel, onOpenPhotoCamera = { handlePhotoButton() }, onOpenVideoCamera = { handleVideoButton() }) }
MainAppTheme {
var showStartupSplash by remember {
mutableStateOf(true)
}
LaunchedEffect(Unit) {
delay(2000)
showStartupSplash = false
}
if (showStartupSplash) {
M530StartupSplash()
} else {
AppNavigation(
viewModel = viewModel,
onOpenPhotoCamera = { handlePhotoButton() },
onOpenVideoCamera = { handleVideoButton() }
)
}
}
}
}
@ -122,7 +315,7 @@ class MainActivity : ComponentActivity() {
if (event?.repeatCount ?: 0 > 0) return true
Log.i(TAG, "M530 KEY DOWN: $keyCode")
if (!isAuthenticated && keyCode in setOf(KeyEvent.KEYCODE_F1, KeyEvent.KEYCODE_F2, KeyEvent.KEYCODE_F3, KeyEvent.KEYCODE_F4, KeyEvent.KEYCODE_F5, KeyEvent.KEYCODE_F7)) {
toast("Inicia sesión para usar la bodycam")
toast("Inicia sesion para usar la bodycam")
return true
}
return when (keyCode) {
@ -182,13 +375,11 @@ class MainActivity : ComponentActivity() {
true
}
KeyEvent.KEYCODE_F4 -> {
val result = deviceController.startPtt()
toast(if (result.isSuccess) "PTT: hablando" else "PTT error: ${result.exceptionOrNull()?.message}")
true
}
KeyEvent.KEYCODE_F5 -> {
// Algunas versiones del firmware también emiten ACTION_VIDEO_DOWN.
// El ViewModel aplica debounce para evitar doble ejecución.
// Algunas versiones del firmware tambien emiten ACTION_VIDEO_DOWN.
// El ViewModel aplica debounce para evitar doble ejecucion.
handleVideoButton()
true
}
@ -200,24 +391,53 @@ class MainActivity : ComponentActivity() {
}
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
Log.i(TAG, "M530 KEY UP: $keyCode")
if (!isAuthenticated && keyCode in setOf(KeyEvent.KEYCODE_F1, KeyEvent.KEYCODE_F2, KeyEvent.KEYCODE_F3, KeyEvent.KEYCODE_F4, KeyEvent.KEYCODE_F5, KeyEvent.KEYCODE_F7)) return true
override fun onKeyUp(
keyCode: Int,
event: KeyEvent?
): Boolean {
Log.i(
TAG,
"M530 KEY UP: $keyCode"
)
if (
!isAuthenticated &&
keyCode in setOf(
KeyEvent.KEYCODE_F1,
KeyEvent.KEYCODE_F2,
KeyEvent.KEYCODE_F3,
KeyEvent.KEYCODE_F4,
KeyEvent.KEYCODE_F5,
KeyEvent.KEYCODE_F7
)
) {
return true
}
return when (keyCode) {
KeyEvent.KEYCODE_F4 -> {
val result = deviceController.stopPtt()
toast(if (result.isSuccess) "PTT: turno liberado" else "PTT error")
true
}
KeyEvent.KEYCODE_F1, KeyEvent.KEYCODE_F2, KeyEvent.KEYCODE_F3,
KeyEvent.KEYCODE_F5, KeyEvent.KEYCODE_F7 -> true
else -> super.onKeyUp(keyCode, event)
KeyEvent.KEYCODE_F1,
KeyEvent.KEYCODE_F2,
KeyEvent.KEYCODE_F3,
KeyEvent.KEYCODE_F5,
KeyEvent.KEYCODE_F7 -> true
else ->
super.onKeyUp(
keyCode,
event
)
}
}
private fun handleVideoButton() {
if (!isAuthenticated) {
toast("Inicia sesión para usar VIDEO")
toast("Inicia sesion para usar VIDEO")
return
}
startActivity(
@ -254,13 +474,40 @@ class MainActivity : ComponentActivity() {
).addFlags(android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP)
)
}
private fun handleSosButton() {
private fun handleSosButton() {
val result = deviceController.triggerSos()
val loc = result.location
val msg = if (loc != null) "SOS registrado (${loc.latitude}, ${loc.longitude})" else "SOS registrado (GPS sin posición reciente)"
Log.w(TAG, msg)
toast(msg)
toast("Enviando SOS...")
deviceController.triggerSos { result ->
runOnUiThread {
if (result.isSuccess) {
val alertId = result.getOrNull()
Log.w(
TAG,
"SOS ENVIADO id=$alertId"
)
toast("SOS ENVIADO")
} else {
val message =
result.exceptionOrNull()?.message
?: "No se pudo enviar SOS"
Log.e(
TAG,
"SOS NO ENVIADO: $message"
)
toast(message)
}
}
}
}
private fun registerHardwareReceiver() {
@ -268,6 +515,9 @@ class MainActivity : ComponentActivity() {
val filter = IntentFilter().apply {
addAction(ACTION_VIDEO_DOWN)
addAction(ACTION_VIDEO_UP)
addAction(ACTION_PTT_DOWN)
addAction(ACTION_PTT_UP)
addAction(ACTION_PTT_LONG_PRESS)
}
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -296,10 +546,14 @@ class MainActivity : ComponentActivity() {
override fun onPause() {
super.onPause()
unregisterHardwareReceiver()
// El receiver permanece activo para capturar
// botones fisicos mientras otra Activity de la app
// (por ejemplo la camara) esta en primer plano.
}
override fun onDestroy() {
unregisterHardwareReceiver()
deviceController.release()
super.onDestroy()
}
@ -361,6 +615,7 @@ fun AppNavigation(viewModel: MainViewModel, onOpenPhotoCamera: () -> Unit, onOpe
LoginScreen(
state = authState,
onLogin = authViewModel::login,
onQrLogin = authViewModel::qrLogin,
onRetrySession = authViewModel::restoreSession,
onClearError = authViewModel::clearError
)
@ -392,10 +647,3 @@ fun AppNavigation(viewModel: MainViewModel, onOpenPhotoCamera: () -> Unit, onOpe
}
}
}