From 1b130a9ed8c87396eaf7704373d036bab4c06662 Mon Sep 17 00:00:00 2001 From: Miguel Grandez Date: Wed, 30 Sep 2026 15:59:13 -0500 Subject: [PATCH] feat: completar integracion M530 backend, telemetria y cierre SOS central --- .../twentyfoulabs/data/auth/AuthApiClient.kt | 20 +++++++++++ .../twentyfoulabs/data/auth/AuthModels.kt | 18 ++++++++++ .../twentyfoulabs/data/auth/AuthRepository.kt | 33 +++++++++++++++++-- .../data/device/M530DeviceController.kt | 11 +++++-- .../twentyfoulabs/ui/screens/MainViewModel.kt | 5 +++ 5 files changed, 82 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/auth/AuthApiClient.kt b/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/auth/AuthApiClient.kt index e09a228..0d79252 100644 --- a/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/auth/AuthApiClient.kt +++ b/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/auth/AuthApiClient.kt @@ -1,5 +1,7 @@ package com.bodycamera.twentyfoulabs.data.auth +import android.util.Log + import com.bodycamera.twentyfoulabs.data.backend.BackendConfig import com.bodycamera.twentyfoulabs.data.backend.DeviceIdentity import kotlinx.coroutines.Dispatchers @@ -44,6 +46,18 @@ class AuthApiClient @Inject constructor( json.decodeFromString(response) } + suspend fun refresh(request: RefreshRequest): RefreshResponse = + withContext(Dispatchers.IO) { + val body = json.encodeToString(request) + val response = request( + "POST", + "/mobile/v1/auth/refresh", + body = body + ) + Log.i("AuthApiClient", "AUTH REFRESH OK") + json.decodeFromString(response) + } + suspend fun me(token: String): MeResponse = withContext(Dispatchers.IO) { val response = request( @@ -173,3 +187,9 @@ class AuthApiException( val statusCode: Int, override val message: String ) : Exception(message) + + + + + + diff --git a/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/auth/AuthModels.kt b/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/auth/AuthModels.kt index 7aa7d8e..542ac50 100644 --- a/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/auth/AuthModels.kt +++ b/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/auth/AuthModels.kt @@ -11,6 +11,11 @@ data class LoginRequest( @SerialName("device_model") val deviceModel: String? = null ) +@Serializable +data class RefreshRequest( + @SerialName("refresh_token") val refreshToken: String +) + @Serializable data class QrLoginRequest( @SerialName("qr_token") val qrToken: String, @@ -63,6 +68,15 @@ data class LoginResponse( } } +@Serializable +data class RefreshResponse( + @SerialName("access_token") val accessToken: String? = null, + @SerialName("refresh_token") val refreshToken: String? = null, + @SerialName("token_type") val tokenType: String? = null, + @SerialName("expires_in") val expiresIn: Int? = null, + @SerialName("server_time") val serverTime: String? = null +) + @Serializable data class MeResponse( val user: AuthUser, @@ -88,3 +102,7 @@ data class AuthSession( val device: AuthDevice? = null, val refreshToken: String? = null ) + + + + diff --git a/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/auth/AuthRepository.kt b/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/auth/AuthRepository.kt index 67f3454..b30c02d 100644 --- a/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/auth/AuthRepository.kt +++ b/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/auth/AuthRepository.kt @@ -127,11 +127,21 @@ session } catch (e: AuthApiException) { - if (e.statusCode == 401) { + if (e.statusCode == 401 && !refreshToken.isNullOrBlank()) { + android.util.Log.i("AuthRepository", "401 inicial detectado - intentando refresh") + val refreshed = api.refresh(RefreshRequest(refreshToken)) + val newToken = refreshed.accessToken?.takeIf { it.isNotBlank() } ?: throw e + val refreshedResponse = try { api.me(newToken) } catch (retryError: AuthApiException) { if (retryError.statusCode == 401) { store.clear(); android.util.Log.i("AuthRepository", "Segundo 401 - sesion eliminada"); throw retryError } else { throw retryError } } + val newRefreshToken = refreshed.refreshToken?.takeIf { it.isNotBlank() } ?: refreshToken + val refreshedSession = AuthSession(token = newToken, user = refreshedResponse.user, device = refreshedResponse.device ?: refreshedResponse.user.assignedDevice, refreshToken = newRefreshToken) + store.save(refreshedSession) + refreshedSession + } else if (e.statusCode == 401) { store.clear() + throw e + } else { + throw e } - - throw e } } @@ -153,3 +163,20 @@ suspend fun logout() { } } } + + + + + + + + + + + + + + + + + diff --git a/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/device/M530DeviceController.kt b/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/device/M530DeviceController.kt index c1483be..33a227d 100644 --- a/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/device/M530DeviceController.kt +++ b/app/src/main/kotlin/com/bodycamera/twentyfoulabs/data/device/M530DeviceController.kt @@ -1,6 +1,7 @@ -package com.bodycamera.twentyfoulabs.data.device +package com.bodycamera.twentyfoulabs.data.device import android.Manifest +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter @@ -82,8 +83,11 @@ class M530DeviceController @Inject constructor( ) } + private val powerReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent?.action == Intent.ACTION_POWER_CONNECTED || intent?.action == Intent.ACTION_POWER_DISCONNECTED) { Log.i(TAG, "Cambio de cargador detectado: ${intent.action}"); logTelemetry() } } } + init { startLocationUpdates() + context.registerReceiver(powerReceiver, IntentFilter().apply { addAction(Intent.ACTION_POWER_CONNECTED); addAction(Intent.ACTION_POWER_DISCONNECTED) }) } @@ -460,7 +464,7 @@ Result.success(file) } if (location == null) { - Log.w(TAG, "GPS: sin ubicación reciente; se omite el envío") + Log.w(TAG, "GPS: sin ubicación reciente; se omite el envío") return } @@ -609,3 +613,6 @@ Result.success(file) + + + diff --git a/app/src/main/kotlin/com/bodycamera/twentyfoulabs/ui/screens/MainViewModel.kt b/app/src/main/kotlin/com/bodycamera/twentyfoulabs/ui/screens/MainViewModel.kt index 366a996..ded9e2f 100644 --- a/app/src/main/kotlin/com/bodycamera/twentyfoulabs/ui/screens/MainViewModel.kt +++ b/app/src/main/kotlin/com/bodycamera/twentyfoulabs/ui/screens/MainViewModel.kt @@ -203,6 +203,10 @@ private fun vibrateRecordingStopped() { result.onSuccess { state -> val previous = centralSosCadenceActive centralSosCadenceActive = state.isSosCadenceActive + if (previous && !centralSosCadenceActive && sosSessionActive.get()) { + Log.i(TAG, "SOS cerrado desde central - finalizando sesion SOS local") + stopVideoFromUi() + } if (previous != centralSosCadenceActive) { Log.i( TAG, @@ -668,3 +672,4 @@ data class DeviceStatus( +