feat: completar integracion M530 backend, telemetria y cierre SOS central

This commit is contained in:
Miguel Angel 2026-09-30 15:59:13 -05:00
parent 41b81c972a
commit 1b130a9ed8
5 changed files with 82 additions and 5 deletions

View file

@ -1,5 +1,7 @@
package com.bodycamera.twentyfoulabs.data.auth package com.bodycamera.twentyfoulabs.data.auth
import android.util.Log
import com.bodycamera.twentyfoulabs.data.backend.BackendConfig import com.bodycamera.twentyfoulabs.data.backend.BackendConfig
import com.bodycamera.twentyfoulabs.data.backend.DeviceIdentity import com.bodycamera.twentyfoulabs.data.backend.DeviceIdentity
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -44,6 +46,18 @@ class AuthApiClient @Inject constructor(
json.decodeFromString<LoginResponse>(response) json.decodeFromString<LoginResponse>(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<RefreshResponse>(response)
}
suspend fun me(token: String): MeResponse = suspend fun me(token: String): MeResponse =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val response = request( val response = request(
@ -173,3 +187,9 @@ class AuthApiException(
val statusCode: Int, val statusCode: Int,
override val message: String override val message: String
) : Exception(message) ) : Exception(message)

View file

@ -11,6 +11,11 @@ data class LoginRequest(
@SerialName("device_model") val deviceModel: String? = null @SerialName("device_model") val deviceModel: String? = null
) )
@Serializable
data class RefreshRequest(
@SerialName("refresh_token") val refreshToken: String
)
@Serializable @Serializable
data class QrLoginRequest( data class QrLoginRequest(
@SerialName("qr_token") val qrToken: String, @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 @Serializable
data class MeResponse( data class MeResponse(
val user: AuthUser, val user: AuthUser,
@ -88,3 +102,7 @@ data class AuthSession(
val device: AuthDevice? = null, val device: AuthDevice? = null,
val refreshToken: String? = null val refreshToken: String? = null
) )

View file

@ -127,11 +127,21 @@ session
} catch (e: AuthApiException) { } 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() store.clear()
throw e
} else {
throw e
} }
throw e
} }
} }
@ -153,3 +163,20 @@ suspend fun logout() {
} }
} }
} }

View file

@ -1,6 +1,7 @@
package com.bodycamera.twentyfoulabs.data.device package com.bodycamera.twentyfoulabs.data.device
import android.Manifest import android.Manifest
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter 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 { init {
startLocationUpdates() 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) { 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 return
} }
@ -609,3 +613,6 @@ Result.success(file)

View file

@ -203,6 +203,10 @@ private fun vibrateRecordingStopped() {
result.onSuccess { state -> result.onSuccess { state ->
val previous = centralSosCadenceActive val previous = centralSosCadenceActive
centralSosCadenceActive = state.isSosCadenceActive centralSosCadenceActive = state.isSosCadenceActive
if (previous && !centralSosCadenceActive && sosSessionActive.get()) {
Log.i(TAG, "SOS cerrado desde central - finalizando sesion SOS local")
stopVideoFromUi()
}
if (previous != centralSosCadenceActive) { if (previous != centralSosCadenceActive) {
Log.i( Log.i(
TAG, TAG,
@ -668,3 +672,4 @@ data class DeviceStatus(