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.

Item Valor
minSdk 26 (Android 8.0)
Linguagem Kotlin + Coroutines
Permissões CAMERA, 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â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.


Antes da primeira captura, provisione o certificado do dispositivo. A chamada é idempotente: se o dispositivo já tem certificado válido, ela retorna AlreadyProvisioned sem gerar chave, sem emitir CSR e sem ir à rede. Chamar a cada lançamento do app é barato e seguro — você não precisa guardar essa chamada com nenhuma verificação própria.

import br.com.provvi.csr.ProvisioningOutcome
lifecycleScope.launch {
when (val outcome = ProvviSDK.provisionDeviceIfNeeded()) {
is ProvisioningOutcome.Success -> {
// Provisionou agora — dispositivo pronto para capturar.
}
is ProvisioningOutcome.AlreadyProvisioned -> {
// Já havia certificado válido (expira em outcome.expiresAt). Nada foi feito.
}
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.

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.

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",
)
)

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.
}
}
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.

As falhas de setup de câmera chegam como estados distintos, e não como um erro genérico:

Situação Estado recebido
Permissão de câmera não concedida CaptureOutcome.PermissionDenied
Dispositivo sem câmera física CaptureOutcome.CaptureError (“câmera física não encontrada”)
Câmera em uso / falha ao abrir a sessão CaptureOutcome.CaptureError (“câmera em uso por outro processo”)
Câmera abriu mas não entregou frame CaptureOutcome.CaptureError (“timeout aguardando frame da câmera”)

Tempo de espera por um frame. No modo automático o SDK desiste após 10 segundos sem frame e devolve o CaptureError de timeout. Na tela de captura com disparo manual não há limite de tempo: o SDK aguarda o usuário enquadrar e tocar no disparador, e a espera é encerrada pelo ciclo de vida da tela (fechar a tela cancela a captura). Falhas de setup não ficam sujeitas a essa espera — elas retornam imediatamente, nos dois modos.


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.


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.