feat: integrar autenticacion QR, sesion y mejoras M530
This commit is contained in:
parent
9deeb40217
commit
0d87c0e4d9
19 changed files with 2322 additions and 212 deletions
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue