Capítulo 18: Permissões em Tempo de Execução, ActivityResultContracts e Câmera

Especialização em Mobile Nativo com Android • Kotlin 2.0 & Jetpack Compose


🗺️ Mapa Conceitual do Fluxo de Permissões no Android Moderno

flowchart TD
    START["Usuário Toca em Ação Sensível (Ex: Tirar Foto)"] --> CHECK{"ContextCompat.checkSelfPermission == GRANTED?"}

    CHECK -->|"Sim"| EXECUTE["Acessa Hardware / Executa Câmera com Sucesso"]
    CHECK -->|"Não"| RATIONALE{"ActivityCompat.shouldShowRequestPermissionRationale?"}

    RATIONALE -->|"Sim (Negou anteriormente)"| DIALOG["Exibe Modal Educativo / Rationale Dialog"]
    DIALOG --> LAUNCH["Dispara Launcher de Permissão"]
    RATIONALE -->|"Não (Primeira vez)"| LAUNCH

    LAUNCH --> RESULT{"Usuário Concedeu no Diálogo do SO?"}
    RESULT -->|"Sim"| EXECUTE
    RESULT -->|"Não (Negado)"| BLOCKED["Exibe Mensagem de Funcionalidade Restrita"]
    RESULT -->|"Não pergunte novamente"| SETTINGS["Direciona para Configurações do Sistema do App"]

    style START fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style CHECK fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style EXECUTE fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style DIALOG fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style BLOCKED fill:#fce4ec,stroke:#e91e63,stroke-width:2px

🏛️ 1. O Modelo de Privacidade e Segurança do Android Moderno

O modelo de segurança do Android evoluiu de permissões estáticas concedidas no momento da instalação para um rígido sistema de autorização granular e revogável a qualquer momento pelo usuário:

  1. Permissões de Instalação vs Permissões Perigosas em Runtime: Permissões como acesso à internet (INTERNET) são concedidas automaticamente na instalação via AndroidManifest.xml. No entanto, permissões que acessam dados privados (Câmera, Localização, Microfone, Contatos) exigem confirmação explícita em tempo de execução através de diálogos do sistema.
  2. A Nova Fronteira do Android 13/14+: A partir do Android 13 (API 33), aplicativos não podem emitir notificações sem a permissão explícita POST_NOTIFICATIONS. Além disso, o acesso a arquivos foi fragmentado em permissões específicas (READ_MEDIA_IMAGES, READ_MEDIA_VIDEO), priorizando o uso do Photo Picker do sistema, que nem sequer exige permissões para que o usuário selecione fotos com segurança.
  3. Activity Result API no Jetpack Compose: O mecanismo antigo baseado em sobrescrever onRequestPermissionsResult na Activity era frágil e sujeito a acoplamentos desnecessários. No Compose, usamos rememberLauncherForActivityResult com contratos nativos (ActivityResultContracts.RequestPermission ou TakePicturePreview), desacoplando o ciclo de vida do componente.

📊 2. Comparativo: Modelo Legado vs Activity Result API no Compose

Critério de Arquitetura Modelo Legado (Activity / Fragment) Modelo Moderno com Activity Result API (Compose)
Ponto de Registro requestPermissions(arrayOf(...), REQUEST_CODE) rememberLauncherForActivityResult(contract) { ... }
Tratamento de Retorno Sobrescrita de métodos com inteiros mágicos (requestCode) Callback de função pura tipada (concedido: Boolean)
Sobrevivência à Recriação Complexo e propenso a perdas se a Activity for destruída O contrato restaura o callback automaticamente pelo framework
Acoplamento com UI Código espalhado entre View e Activity Totalmente contido no escopo declarativo do Composable
Suporte a Câmera/Galeria Criação manual de Intents implícitas com URIs Contratos prontos: TakePicture, PickVisualMedia

💻 3. Laboratório Prático: Solicitação de Permissão com Rationale no Compose

O exemplo a seguir demonstra a implementação idiomática da checagem e solicitação da permissão de câmera, com apresentação de rationale educativo caso tenha havido recusa prévia:

package br.com.portal.mobile.android.cap18

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat

@Composable
fun BotaoAcessoCamera(
    onPermissaoGarantida: () -> Unit,
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current
    var exibirModalExplicativo by remember { mutableStateOf(false) }

    // Launcher do contrato nativo de solicitação de permissão
    val permissaoLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission()
    ) { concedida: Boolean ->
        if (concedida) {
            onPermissaoGarantida()
        } else {
            exibirModalExplicativo = true
        }
    }

    Column(modifier = modifier.padding(16.dp)) {
        Button(
            onClick = {
                val status = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
                if (status == PackageManager.PERMISSION_GRANTED) {
                    onPermissaoGarantida()
                } else {
                    permissaoLauncher.launch(Manifest.permission.CAMERA)
                }
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("Acionar Câmera do Dispositivo")
        }

        // Rationale Dialog informativo
        if (exibirModalExplicativo) {
            AlertDialog(
                onDismissRequest = { exibirModalExplicativo = false },
                title = { Text("Acesso à Câmera Necessário") },
                text = {
                    Text("O recurso de captura de recibos e comprovantes precisa de autorização da câmera para digitalizar os documentos. Por favor, habilite nas permissões do app.")
                },
                confirmButton = {
                    TextButton(onClick = { exibirModalExplicativo = false }) {
                        Text("Compreendi")
                    }
                }
            )
        }
    }
}

⚠️ 4. Armadilhas Comuns no Tratamento de Permissões

  1. Solicitar Permissões no Arranque Inicial do App: Abrir o aplicativo solicitando Câmera, Localização e Microfone imediatamente na primeira tela assusta os usuários e resulta em alta taxa de recusas. Sempre solicite no momento exato em que o usuário aciona a funcionalidade relacionada (In-Context Permission Request).
  2. Esquecer de Declarar no AndroidManifest.xml: O Android só permite solicitar permissões em tempo de execução se elas já estiverem previamente declaradas com a tag <uses-permission> no manifesto. Omitir no XML faz a solicitação ser ignorada pelo sistema sem nenhum aviso.
  3. Não Tratar a Opção “Não Perguntar Novamente”: Quando o usuário seleciona essa opção, o sistema não exibirá mais o diálogo de permissão e retornará false imediatamente. Nesses casos, o app deve oferecer um botão que direcione o usuário diretamente para as Configurações do Sistema via Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).