package com.bodycamera.twentyfoulabs import android.Manifest import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.util.Log import android.view.KeyEvent import android.view.View import android.view.WindowManager import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.OnBackPressedCallback import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.core.content.ContextCompat import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.bodycamera.twentyfoulabs.data.device.M530DeviceController import com.bodycamera.twentyfoulabs.data.auth.AuthSessionStore import com.bodycamera.twentyfoulabs.navigation.LocalNavigationHandler import com.bodycamera.twentyfoulabs.navigation.rememberNavigationHandler import com.bodycamera.twentyfoulabs.ui.screens.ConnectionSettingsScreen import com.bodycamera.twentyfoulabs.ui.screens.ConnectionSettingsViewModel import com.bodycamera.twentyfoulabs.ui.screens.MainScreen import com.bodycamera.twentyfoulabs.ui.screens.MainViewModel import com.bodycamera.twentyfoulabs.ui.screens.AuthViewModel import com.bodycamera.twentyfoulabs.ui.screens.AuthUiState import com.bodycamera.twentyfoulabs.ui.screens.LoginScreen import com.bodycamera.twentyfoulabs.ui.screens.VideoConfigScreen import com.bodycamera.twentyfoulabs.ui.screens.VideoConfigViewModel import com.bodycamera.twentyfoulabs.ui.theme.MainAppTheme import dagger.hilt.android.AndroidEntryPoint import androidx.hilt.navigation.compose.hiltViewModel import javax.inject.Inject import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch import kotlinx.coroutines.flow.collect @AndroidEntryPoint class MainActivity : ComponentActivity() { private val viewModel: MainViewModel by viewModels() @Inject lateinit var deviceController: M530DeviceController @Inject lateinit var authSessionStore: AuthSessionStore @Volatile private var isAuthenticated = false companion object { 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 var isReceiverRegistered = false private val hardwareReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { ACTION_VIDEO_DOWN -> handleVideoButton() ACTION_VIDEO_UP -> Unit } } } private val permissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { permissions -> val denied = permissions.filterValues { !it }.keys if (denied.isNotEmpty()) { Toast.makeText(this, "Permisos pendientes: ${denied.joinToString()}", Toast.LENGTH_LONG).show() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.i(TAG, "M530 Controller onCreate") window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN) window.setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) requestPermissions() onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() = Unit }) lifecycleScope.launch { authSessionStore.token.collect { token -> isAuthenticated = !token.isNullOrBlank() } } registerHardwareReceiver() deviceController.logTelemetry() setContent { MainAppTheme { AppNavigation(viewModel = viewModel, onOpenPhotoCamera = { handlePhotoButton() }, onOpenVideoCamera = { handleVideoButton() }) } } } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { 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") return true } return when (keyCode) { KeyEvent.KEYCODE_F1 -> { toast("Marcando evidencia como favorita...") deviceController.markEvidence( "F1_STAR" ) { result -> runOnUiThread { if (result.isSuccess) { val mediaId = result.getOrNull() toast( "Evidencia favorita registrada (ID $mediaId)" ) } else { toast( result.exceptionOrNull()?.message ?: "No se pudo marcar favorito" ) } } } true } KeyEvent.KEYCODE_F2 -> { handlePhotoButton() true } KeyEvent.KEYCODE_F3 -> { if (deviceController.isMicRecording()) { val result = deviceController.stopMicRecording() toast( if (result.isSuccess) "MIC: grabacion guardada" else "MIC error: ${result.exceptionOrNull()?.message}" ) } else { val result = deviceController.startMicRecording() toast( if (result.isSuccess) "MIC: grabando" else "MIC error: ${result.exceptionOrNull()?.message}" ) } 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. handleVideoButton() true } KeyEvent.KEYCODE_F7 -> { handleSosButton() true } else -> super.onKeyDown(keyCode, event) } } 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) } } private fun handleVideoButton() { if (!isAuthenticated) { toast("Inicia sesión para usar VIDEO") return } startActivity( android.content.Intent( this, com.bodycamera.twentyfoulabs.camera.M530CameraActivity::class.java ).putExtra( com.bodycamera.twentyfoulabs.camera.M530CameraActivity.EXTRA_MODE, com.bodycamera.twentyfoulabs.camera.M530CameraActivity.MODE_VIDEO ).addFlags(android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP) ) } private fun handlePhotoButton() { startActivity( android.content.Intent( this, com.bodycamera.twentyfoulabs.camera.M530CameraActivity::class.java ).addFlags(android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP) ) } 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) } private fun registerHardwareReceiver() { if (isReceiverRegistered) return val filter = IntentFilter().apply { addAction(ACTION_VIDEO_DOWN) addAction(ACTION_VIDEO_UP) } try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { registerReceiver(hardwareReceiver, filter, RECEIVER_EXPORTED) } else { @Suppress("DEPRECATION") registerReceiver(hardwareReceiver, filter) } isReceiverRegistered = true } catch (t: Throwable) { Log.e(TAG, "No se pudo registrar receiver", t) } } private fun unregisterHardwareReceiver() { if (!isReceiverRegistered) return try { unregisterReceiver(hardwareReceiver) } catch (_: Throwable) {} isReceiverRegistered = false } override fun onResume() { super.onResume() hideSystemUI() registerHardwareReceiver() } override fun onPause() { super.onPause() unregisterHardwareReceiver() } override fun onDestroy() { deviceController.release() super.onDestroy() } override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) if (hasFocus) hideSystemUI() } private fun hideSystemUI() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false) window.insetsController?.let { controller -> controller.hide(android.view.WindowInsets.Type.statusBars()) controller.hide(android.view.WindowInsets.Type.navigationBars()) controller.systemBarsBehavior = android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } } else { @Suppress("DEPRECATION") window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION } } private fun requestPermissions() { val requested = listOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO ).filter { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED } if (requested.isNotEmpty()) permissionLauncher.launch(requested.toTypedArray()) } private fun toast(message: String) = Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } @Composable fun AppNavigation(viewModel: MainViewModel, onOpenPhotoCamera: () -> Unit, onOpenVideoCamera: () -> Unit) { val navController = rememberNavController() val navigationHandler = rememberNavigationHandler(navController) val authViewModel: AuthViewModel = hiltViewModel() val authState by authViewModel.state.collectAsState() LaunchedEffect(authState) { if (authState is AuthUiState.Authenticated && navController.currentDestination?.route != "main") { navController.navigate("main") { popUpTo("login") { inclusive = true } } } } CompositionLocalProvider(LocalNavigationHandler provides navigationHandler) { NavHost( navController = navController, startDestination = "login" ) { composable("login") { LoginScreen( state = authState, onLogin = authViewModel::login, onRetrySession = authViewModel::restoreSession, onClearError = authViewModel::clearError ) } composable("main") { MainScreen( viewModel = viewModel, onNavigateToSettings = { navController.navigate("connection_settings") }, onNavigateToVideoConfig = { navController.navigate("video_config") }, onOpenPhotoCamera = onOpenPhotoCamera, onOpenVideoCamera = onOpenVideoCamera, onLogout = { authViewModel.logout() navController.navigate("login") { popUpTo("main") { inclusive = true } } } ) } composable("connection_settings") { val settingsViewModel: ConnectionSettingsViewModel = hiltViewModel() ConnectionSettingsScreen(viewModel = settingsViewModel) } composable("video_config") { val videoConfigViewModel: VideoConfigViewModel = hiltViewModel() VideoConfigScreen(viewModel = videoConfigViewModel) } } } }