Pular para o conteúdo

Android SDK

SDK nativo Kotlin para captura autenticada com assinatura ICP-Brasil. O SDK é dono da experiência de captura (preview ao vivo + botão de disparo + máscara-guia opcional) — o app integrador cuida apenas da navegação, do contexto de negócio e de exibir o resultado.

ItemValor
minSdk26 (Android 8.0)
LinguagemKotlin + Coroutines
PermissõesCAMERA, ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, INTERNET

O SDK é distribuído como arquivo .aar fornecido pela equipe Provvi.

  1. Copie provvi-sdk-release.aar para o diretório libs/ do seu módulo Android.
  2. Declare a dependência:
// build.gradle.kts (módulo)
dependencies {
implementation(files("libs/provvi-sdk-release.aar"))
}

A integração tem quatro passos. Os dois primeiros rodam uma vez por sessão do app; os dois últimos, uma vez por captura.

1. configure(...) → inicializa o SDK e valida a licença (uma vez, no startup)
2. provisionDeviceIfNeeded() → provisiona o certificado do dispositivo (uma vez)
3. startSecureCapture(...) → abre a tela de captura do SDK (por captura)
4. consumeLastCaptureOutcome() → lê o resultado da captura (por captura)

O ponto de entrada é o objeto ProvviSDK. Todas as chamadas de rede, assinatura e orquestração ficam internas ao SDK — o integrador nunca monta manifesto, nem fala com o backend diretamente.


Chame ProvviSDK.configure(...) uma vez no startup do app (por exemplo, antes de abrir a tela de captura). É uma função suspend — execute dentro de uma coroutine.

import br.com.provvi.ProvviSDK
import br.com.provvi.CaptureProfile
import br.com.provvi.ProvviSDKError
lifecycleScope.launch {
try {
ProvviSDK.configure(
context = applicationContext,
licenseKey = BuildConfig.PROVVI_LICENSE_KEY, // pvv_live_* / pvv_sand_* / pvv_stag_*
operatorId = operatorId, // identidade que assina (ver abaixo)
captureProfile = CaptureProfile.VEHICLE_INSPECTION,
)
} catch (e: ProvviSDKError.AlreadyConfigured) {
// Já configurado nesta sessão — seguro ignorar.
}
}
ParâmetroTipoDefaultDescrição
contextContextObrigatório. Use o applicationContext.
licenseKeyStringObrigatório. Chave de licença pvv_live_* / pvv_sand_* / pvv_stag_*.
operatorIdStringObrigatório. UUID do operador (vistoriador) recebido no onboarding. Seleciona o certificado do dispositivo e precisa estar ACTIVE.
captureProfileCaptureProfileVEHICLE_INSPECTIONPerfil de captura. KYC_SELFIE usa a câmera frontal.

A validação da licença acontece dentro de configure(...). Se a chave for inválida, expirada, revogada ou estiver acima do volume contratado, a chamada falha com erro de licença — trate-o antes de prosseguir para a captura. Consulte Licenciamento.

Identidade do app é derivada automaticamente. O SDK identifica o app (package name + certificado de assinatura) lendo o próprio APK — você não informa esses dados. A persistência opcional do JPEG é definida pela sua licença (política de contrato), não por parâmetro de código.


Antes da primeira captura, provisione o certificado do dispositivo. É uma operação única por instalação (idempotente do ponto de vista do integrador — chame quando ainda não houver certificado).

import br.com.provvi.csr.ProvisioningOutcome
lifecycleScope.launch {
when (val outcome = ProvviSDK.provisionDeviceIfNeeded()) {
is ProvisioningOutcome.Success -> {
// Dispositivo pronto para capturar.
}
is ProvisioningOutcome.Failure -> {
showError("Falha no provisionamento: ${outcome.cause.message}")
}
}
}

O provisionamento é fail-closed: se falhar, a captura não deve prosseguir. A chave do dispositivo é gerada em hardware (Android Keystore) e nunca sai do aparelho.


A captura usa a tela do SDK (ProvviCaptureActivity): preview ao vivo, botão de disparo e, opcionalmente, uma máscara-guia. O integrador lança a tela e recebe o resultado.

Registre um ActivityResultLauncher no onCreate da sua Activity:

import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import android.app.Activity
import br.com.provvi.ProvviSDK
import br.com.provvi.CaptureOutcome
private val captureLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode != Activity.RESULT_OK) {
// Usuário cancelou a captura.
return@registerForActivityResult
}
// O resultado completo fica retido no SDK — leia uma vez:
when (val outcome = ProvviSDK.consumeLastCaptureOutcome()) {
is CaptureOutcome.Success -> handleSuccess(outcome.result)
is CaptureOutcome.BackendError -> handleBackendError(outcome)
is CaptureOutcome.LicenseError -> showError(outcome.error.message)
else -> showError("Captura não concluída")
}
}

Por que o resultado é lido por consumeLastCaptureOutcome() e não pelo Intent? O JPEG assinado pode ter vários MB e estouraria o limite de transporte do ActivityResult. O SDK retém o resultado e o entrega de forma segura, independente do tamanho da imagem. É consumo único — leia uma vez por captura.

import br.com.provvi.assertions.MapAssertions
import br.com.provvi.ui.MaskConfig
import br.com.provvi.ui.MaskShape
val maskConfig = MaskConfig(
shape = MaskShape.RECTANGLE,
label = "Enquadre o veículo na moldura",
)
val assertions = MapAssertions(
mapOf(
"captured_by" to "operador-123",
"reference_id" to "VIN-9BWZZZ377VT004251",
)
)
ProvviSDK.startSecureCapture(
launcher = captureLauncher,
context = this,
maskConfig = maskConfig, // opcional — null ou shape NONE = sem máscara
assertions = assertions, // opcional — metadados gravados no manifesto
)

Se você não usa ActivityResultLauncher (por exemplo, em uma ponte Flutter via startActivityForResult), monte o Intent diretamente:

import br.com.provvi.ui.ProvviCaptureActivity
val intent = ProvviCaptureActivity.createIntent(
context = this,
maskConfig = maskConfig, // opcional
assertionsJson = assertionsJsonString, // opcional, mapa de assertions serializado em JSON
)
startActivityForResult(intent, REQUEST_CAPTURE)
// No onActivityResult: leia ProvviSDK.consumeLastCaptureOutcome()

A máscara é apenas visual — orienta o enquadramento e nunca altera a imagem assinada.

CampoTipoDefaultDescrição
shapeMaskShapeSQUARERECTANGLE, OVAL, SQUARE ou NONE (sem máscara).
sizeRatioFloat0.72Tamanho da moldura relativo ao menor lado da tela (0–1).
colorIntteal ProvviCor ARGB da borda.
lineWidthDpFloat2.5Espessura da borda em dp.
dimOutsideFloat0.45Opacidade do escurecimento fora da moldura (0–1).
labelString?”Enquadre o objeto na moldura”Texto-guia; null/vazio = sem texto.
labelPositionMaskLabelPositionBOTTOMTOP, BOTTOM ou CENTER.
aspectRatioFloat?por formaProporção largura÷altura; null usa o default da forma.

Os campos passados como assertions são gravados verbatim no manifesto C2PA da captura (assertion com.provvi.capture). Use-os para correlacionar a captura ao seu contexto de negócio — identificador da apólice, do operador, da vistoria, etc.

import br.com.provvi.assertions.MapAssertions
val assertions = MapAssertions(
mapOf(
"captured_by" to "operador-123",
"reference_id" to "policy-12345",
"inspector_name" to "João Silva",
)
)

CampoTipoDescrição
sessionIdStringIdentificador único da captura.
authenticatedJpegBytesByteArray?JPEG com o manifesto C2PA + assinatura ICP-Brasil embutidos.

A prova de autenticidade viaja dentro do JPEG — não há URL de manifesto separada a tratar em runtime. O JPEG é autoverificável: o manifesto pode ser extraído e verificado em verify.provvi.com.br. GPS, integridade, horário e demais sinais já estão embarcados no manifesto.

private fun handleSuccess(result: ProvviCaptureResult) {
val sessionId = result.sessionId
result.authenticatedJpegBytes?.let { jpeg ->
// Salvar, exibir ou enviar ao seu backend.
}
}
when (val outcome = ProvviSDK.consumeLastCaptureOutcome()) {
is CaptureOutcome.Success -> handleSuccess(outcome.result)
is CaptureOutcome.BackendError -> {
// Falha ao concluir no backend (ex.: indisponibilidade temporária).
// outcome.result pode conter a imagem se a assinatura local foi concluída.
showRetry(outcome.message)
}
is CaptureOutcome.LicenseError ->
showError(outcome.error.message) // licença inválida/expirada/revogada/sem volume
is CaptureOutcome.PermissionDenied ->
showError("Permissão de câmera ou localização negada")
is CaptureOutcome.DeviceCompromised ->
showError("Dispositivo não íntegro (root/emulador)")
is CaptureOutcome.ClockSuspicious ->
showError("Relógio do dispositivo alterado")
is CaptureOutcome.SigningFailed ->
showError("Falha ao assinar a captura")
is CaptureOutcome.CaptureError ->
showError("Erro na captura")
null ->
showError("Nenhum resultado de captura disponível")
else -> showError("Captura não concluída")
}

Os estados DeviceCompromised, MockLocationDetected e ClockSuspicious indicam que o SDK bloqueou a captura por suspeita de fraude — o app deve apresentar a situação ao usuário, não tentar contornar.


SituaçãoVariante / tipoRecuperável
Câmera ou localização negadaPermissionDeniedSim — solicitar permissões
Dispositivo com root/emuladorDeviceCompromisedNão
Localização simuladaMockLocationDetectedNão
Relógio alteradoClockSuspiciousNão
Falha na assinaturaSigningFailedSim (retry)
Erro de captura/câmeraCaptureErrorÀs vezes (retry)
Falha no backendBackendErrorSim (retry)
Licença ausente/inválida/expirada/revogada/sem volumeLicenseErrorDepende — ver mensagem

Consulte Erros para a tabela completa de códigos e tratamento, e Licenciamento para os erros de licença.


O SDK não solicita permissões de localização em runtime — isso é responsabilidade do app. A tela de captura solicita CAMERA automaticamente se faltar, mas a localização precisa estar concedida antes de iniciar a captura.

AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
private val PERMS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
private fun ensurePermissions(onGranted: () -> Unit) {
val missing = PERMS.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (missing.isEmpty()) { onGranted(); return }
ActivityCompat.requestPermissions(this, missing.toTypedArray(), REQ_PROVVI_PERMS)
// Em onRequestPermissionsResult, rechecar e só então iniciar a captura.
}

ACCESS_FINE_LOCATION e ACCESS_COARSE_LOCATION formam um par em Android 12+ — conceda as duas. ACCESS_BACKGROUND_LOCATION não é necessária (o SDK só usa GPS em primeiro plano).


import android.app.Activity
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import br.com.provvi.*
import br.com.provvi.assertions.MapAssertions
import br.com.provvi.csr.ProvisioningOutcome
import br.com.provvi.ui.MaskConfig
import br.com.provvi.ui.MaskShape
import kotlinx.coroutines.launch
class CapturaActivity : AppCompatActivity() {
private val captureLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
when (val outcome = ProvviSDK.consumeLastCaptureOutcome()) {
is CaptureOutcome.Success -> {
val jpeg = outcome.result.authenticatedJpegBytes
showSuccess("Sessão: ${outcome.result.sessionId}")
}
is CaptureOutcome.LicenseError -> showError(outcome.error.message)
else -> showError("Captura não concluída")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_captura)
lifecycleScope.launch {
// 1. Configurar (idempotente por sessão)
try {
ProvviSDK.configure(
context = applicationContext,
licenseKey = BuildConfig.PROVVI_LICENSE_KEY,
operatorId = intent.getStringExtra("operator") ?: return@launch,
captureProfile = CaptureProfile.VEHICLE_INSPECTION,
)
} catch (e: ProvviSDKError.AlreadyConfigured) { /* ok */ }
// 2. Provisionar (uma vez)
if (ProvviSDK.provisionDeviceIfNeeded() is ProvisioningOutcome.Failure) {
showError("Falha no provisionamento")
return@launch
}
// 3. Lançar a captura (após garantir permissões)
ensurePermissions {
ProvviSDK.startSecureCapture(
launcher = captureLauncher,
context = this@CapturaActivity,
maskConfig = MaskConfig(shape = MaskShape.RECTANGLE),
assertions = MapAssertions(
mapOf(
"reference_id" to (intent.getStringExtra("vin") ?: ""),
"captured_by" to (intent.getStringExtra("operator") ?: ""),
)
),
)
}
}
}
}

Para trocar de operador ou licença na mesma sessão do app, chame ProvviSDK.reset() antes de configure(...) novamente. O reset() limpa apenas o estado local do SDK — não revoga o certificado do dispositivo nem altera nada no backend.