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.
Requisitos
Seção intitulada “Requisitos”| Item | Valor |
|---|---|
| minSdk | 26 (Android 8.0) |
| Linguagem | Kotlin + Coroutines |
| Permissões | CAMERA, ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, INTERNET |
Instalação
Seção intitulada “Instalação”O SDK é distribuído como arquivo .aar fornecido pela equipe Provvi.
- Copie
provvi-sdk-release.aarpara o diretóriolibs/do seu módulo Android. - Declare a dependência:
// build.gradle.kts (módulo)dependencies { implementation(files("libs/provvi-sdk-release.aar"))}Visão geral do fluxo
Seção intitulada “Visão geral do fluxo”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.
1. Inicialização
Seção intitulada “1. Inicialização”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.ProvviSDKimport br.com.provvi.CaptureProfileimport 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âmetros
Seção intitulada “Parâmetros”| Parâmetro | Tipo | Default | Descrição |
|---|---|---|---|
context | Context | — | Obrigatório. Use o applicationContext. |
licenseKey | String | — | Obrigatório. Chave de licença pvv_live_* / pvv_sand_* / pvv_stag_*. |
operatorId | String | — | Obrigatório. UUID do operador (vistoriador) recebido no onboarding. Seleciona o certificado do dispositivo e precisa estar ACTIVE. |
captureProfile | CaptureProfile | VEHICLE_INSPECTION | Perfil 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.
2. Provisionamento do dispositivo
Seção intitulada “2. Provisionamento do dispositivo”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.
3. Captura
Seção intitulada “3. Captura”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.
3.1 Registrar o launcher
Seção intitulada “3.1 Registrar o launcher”Registre um ActivityResultLauncher no onCreate da sua Activity:
import androidx.activity.result.ActivityResultimport androidx.activity.result.contract.ActivityResultContractsimport android.app.Activityimport br.com.provvi.ProvviSDKimport 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 peloIntent? O JPEG assinado pode ter vários MB e estouraria o limite de transporte doActivityResult. O SDK retém o resultado e o entrega de forma segura, independente do tamanho da imagem. É consumo único — leia uma vez por captura.
3.2 Lançar a captura
Seção intitulada “3.2 Lançar a captura”import br.com.provvi.assertions.MapAssertionsimport br.com.provvi.ui.MaskConfigimport 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)Integração via Flutter / outras pontes
Seção intitulada “Integração via Flutter / outras pontes”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()4. Máscara-guia (MaskConfig)
Seção intitulada “4. Máscara-guia (MaskConfig)”A máscara é apenas visual — orienta o enquadramento e nunca altera a imagem assinada.
| Campo | Tipo | Default | Descrição |
|---|---|---|---|
shape | MaskShape | SQUARE | RECTANGLE, OVAL, SQUARE ou NONE (sem máscara). |
sizeRatio | Float | 0.72 | Tamanho da moldura relativo ao menor lado da tela (0–1). |
color | Int | teal Provvi | Cor ARGB da borda. |
lineWidthDp | Float | 2.5 | Espessura da borda em dp. |
dimOutside | Float | 0.45 | Opacidade do escurecimento fora da moldura (0–1). |
label | String? | ”Enquadre o objeto na moldura” | Texto-guia; null/vazio = sem texto. |
labelPosition | MaskLabelPosition | BOTTOM | TOP, BOTTOM ou CENTER. |
aspectRatio | Float? | por forma | Proporção largura÷altura; null usa o default da forma. |
5. Metadados (assertions)
Seção intitulada “5. Metadados (assertions)”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", ))6. Resultado
Seção intitulada “6. Resultado”Sucesso — ProvviCaptureResult
Seção intitulada “Sucesso — ProvviCaptureResult”| Campo | Tipo | Descrição |
|---|---|---|
sessionId | String | Identificador único da captura. |
authenticatedJpegBytes | ByteArray? | 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. }}Tratamento de CaptureOutcome
Seção intitulada “Tratamento de CaptureOutcome”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,MockLocationDetectedeClockSuspiciousindicam que o SDK bloqueou a captura por suspeita de fraude — o app deve apresentar a situação ao usuário, não tentar contornar.
7. Erros
Seção intitulada “7. Erros”| Situação | Variante / tipo | Recuperável |
|---|---|---|
| Câmera ou localização negada | PermissionDenied | Sim — solicitar permissões |
| Dispositivo com root/emulador | DeviceCompromised | Não |
| Localização simulada | MockLocationDetected | Não |
| Relógio alterado | ClockSuspicious | Não |
| Falha na assinatura | SigningFailed | Sim (retry) |
| Erro de captura/câmera | CaptureError | Às vezes (retry) |
| Falha no backend | BackendError | Sim (retry) |
| Licença ausente/inválida/expirada/revogada/sem volume | LicenseError | Depende — ver mensagem |
Consulte Erros para a tabela completa de códigos e tratamento, e Licenciamento para os erros de licença.
8. Permissões
Seção intitulada “8. Permissões”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.
<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_LOCATIONeACCESS_COARSE_LOCATIONformam um par em Android 12+ — conceda as duas.ACCESS_BACKGROUND_LOCATIONnão é necessária (o SDK só usa GPS em primeiro plano).
9. Exemplo completo
Seção intitulada “9. Exemplo completo”import android.app.Activityimport android.os.Bundleimport androidx.activity.result.contract.ActivityResultContractsimport androidx.appcompat.app.AppCompatActivityimport androidx.lifecycle.lifecycleScopeimport br.com.provvi.*import br.com.provvi.assertions.MapAssertionsimport br.com.provvi.csr.ProvisioningOutcomeimport br.com.provvi.ui.MaskConfigimport br.com.provvi.ui.MaskShapeimport 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") ?: ""), ) ), ) } } }}Reconfiguração
Seção intitulada “Reconfiguração”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.