refactor(android): share auth and clean uploaded signatures

This commit is contained in:
2026-09-08 17:16:55 -03:00
parent 516445fea6
commit 33584d545f
@@ -4,15 +4,12 @@ import android.content.Context
import com.korexlabs.dhinspeccion.BuildConfig
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.HttpException
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.Body
@@ -303,14 +300,10 @@ private interface MobileActsApi {
@Path("visitId") visitId: String,
@Body request: MobileCloseVisitRequest,
): VisitDetail
@POST("auth/mobile/refresh")
suspend fun refresh(@Body request: RefreshRequest): MobileSessionResponse
}
class MobileActsRepository(context: Context) {
private val store = SecureSessionStore(context.applicationContext)
private val refreshMutex = Mutex()
private val sessions = MobileSessionCoordinator.get(context.applicationContext)
private val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
private val api: MobileActsApi = Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
@@ -319,11 +312,11 @@ class MobileActsRepository(context: Context) {
.build()
.create(MobileActsApi::class.java)
suspend fun list(visitId: String): MobileActListResponse = authorized { session ->
suspend fun list(visitId: String): MobileActListResponse = sessions.authorized { session ->
api.listActs("Bearer ${session.accessToken}", visitId)
}
suspend fun get(actId: String): MobileActDetail = authorized { session ->
suspend fun get(actId: String): MobileActDetail = sessions.authorized { session ->
api.act("Bearer ${session.accessToken}", actId)
}
@@ -332,7 +325,7 @@ class MobileActsRepository(context: Context) {
assetId: String,
visitCode: String,
urgency: String = "NON_URGENT",
): MobileActDetail = authorized { session ->
): MobileActDetail = sessions.authorized { session ->
api.createAct(
"Bearer ${session.accessToken}",
visitId,
@@ -350,20 +343,20 @@ class MobileActsRepository(context: Context) {
val detail = get(actId)
if (detail.assets.any { it.id == assetId }) return detail
val ids = (detail.assets.map { it.id } + assetId).distinct()
return authorized { session ->
return sessions.authorized { session ->
api.updateAct("Bearer ${session.accessToken}", actId, UpdateMobileActRequest(ids))
}
}
suspend fun closure(actId: String): MobileActClosure = authorized { session ->
suspend fun closure(actId: String): MobileActClosure = sessions.authorized { session ->
api.closure("Bearer ${session.accessToken}", actId)
}
suspend fun setResponsible(actId: String, request: MobileResponsibleRequest): MobileActClosure = authorized { session ->
suspend fun setResponsible(actId: String, request: MobileResponsibleRequest): MobileActClosure = sessions.authorized { session ->
api.responsible("Bearer ${session.accessToken}", actId, request)
}
suspend fun lock(actId: String): MobileActClosure = authorized { session ->
suspend fun lock(actId: String): MobileActClosure = sessions.authorized { session ->
api.lock("Bearer ${session.accessToken}", actId)
}
@@ -394,7 +387,7 @@ class MobileActsRepository(context: Context) {
statement = statement,
)
suspend fun companyOutcome(actId: String, status: String, reason: String): MobileActClosure = authorized { session ->
suspend fun companyOutcome(actId: String, status: String, reason: String): MobileActClosure = sessions.authorized { session ->
api.companyOutcome(
"Bearer ${session.accessToken}",
actId,
@@ -402,11 +395,11 @@ class MobileActsRepository(context: Context) {
)
}
suspend fun seal(actId: String): MobileActClosure = authorized { session ->
suspend fun seal(actId: String): MobileActClosure = sessions.authorized { session ->
api.sealAct("Bearer ${session.accessToken}", actId, MobileSealActRequest())
}
suspend fun closeVisit(visitId: String): VisitDetail = authorized { session ->
suspend fun closeVisit(visitId: String): VisitDetail = sessions.authorized { session ->
api.closeVisit("Bearer ${session.accessToken}", visitId, MobileCloseVisitRequest())
}
@@ -419,53 +412,35 @@ class MobileActsRepository(context: Context) {
company: Boolean,
manifestation: String? = null,
statement: String? = null,
): MobileActClosure = authorized { session ->
val text = "text/plain".toMediaType()
val file = MultipartBody.Part.createFormData(
"file",
png.name,
png.asRequestBody("image/png".toMediaType()),
)
val consent = "true".toRequestBody(text)
val signedAt = Instant.now().toString().toRequestBody(text)
val device = "DH Android".toRequestBody(text)
val lat = latitude?.toString()?.toRequestBody(text)
val lon = longitude?.toString()?.toRequestBody(text)
val accuracy = accuracyM?.toString()?.toRequestBody(text)
if (company) {
api.signCompany(
"Bearer ${session.accessToken}", actId, file, consent, signedAt,
lat, lon, accuracy, device,
manifestation?.toRequestBody(text),
statement?.trim()?.takeIf { it.isNotBlank() }?.toRequestBody(text),
)
} else {
api.signInspector(
"Bearer ${session.accessToken}", actId, file, consent, signedAt,
lat, lon, accuracy, device,
): MobileActClosure {
val response = sessions.authorized { session ->
val text = "text/plain".toMediaType()
val filePart = MultipartBody.Part.createFormData(
"file",
png.name,
png.asRequestBody("image/png".toMediaType()),
)
val consent = "true".toRequestBody(text)
val signedAt = Instant.now().toString().toRequestBody(text)
val device = "DH Android".toRequestBody(text)
val lat = latitude?.toString()?.toRequestBody(text)
val lon = longitude?.toString()?.toRequestBody(text)
val accuracy = accuracyM?.toString()?.toRequestBody(text)
if (company) {
api.signCompany(
"Bearer ${session.accessToken}", actId, filePart, consent, signedAt,
lat, lon, accuracy, device,
manifestation?.toRequestBody(text),
statement?.trim()?.takeIf { it.isNotBlank() }?.toRequestBody(text),
)
} else {
api.signInspector(
"Bearer ${session.accessToken}", actId, filePart, consent, signedAt,
lat, lon, accuracy, device,
)
}
}
}
private suspend fun <T> authorized(block: suspend (StoredSession) -> T): T {
var session = store.load() ?: throw IllegalStateException("Sesión no iniciada")
try {
return block(session)
} catch (error: HttpException) {
if (error.code() != 401) throw error
}
session = refresh(session.refreshToken)
return block(session)
}
private suspend fun refresh(previousRefreshToken: String): StoredSession = refreshMutex.withLock {
val latest = store.load() ?: throw IllegalStateException("Sesión no iniciada")
if (latest.refreshToken != previousRefreshToken) return@withLock latest
try {
store.save(api.refresh(RefreshRequest(previousRefreshToken)))
} catch (error: Throwable) {
store.clear()
throw error
}
png.delete()
return response
}
}