Exemplos de Código: Capítulo 18 — Permissões em Tempo de Execução, Câmera e GPS

Especialização em Mobile Nativo com Android • Demonstrações Práticas Compiláveis


💻 Código de Demonstração: ActivityResultContracts, Permissões em Runtime e Câmera

O código abaixo demonstra o fluxo moderno de solicitação de permissões sensíveis no Jetpack Compose (compatível com Android 13/14+), além da captura direta de fotos via câmera nativa:

  1. Verificação de permissões com ContextCompat.checkSelfPermission.
  2. Registro de callbacks desacoplados via rememberLauncherForActivityResult com contratos nativos (RequestPermission e TakePicturePreview).
  3. Tratamento de permissão negada, exibição de Rationale dialog e suporte à nova permissão de notificações POST_NOTIFICATIONS (Android 13+ / API 33).
  4. Renderização do bitmap capturado em um componente Image do Compose.
package br.com.portal.mobile.android.cap18

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.launch
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat

// =====================================================================
// 1. Tela de Demonstração de Recursos Nativos e Permissões
// =====================================================================

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PermissoesRecursosScreen() {
    val context = LocalContext.current

    // Estados locais para controle de UI
    var fotoCapturada by remember { mutableStateOf<Bitmap?>(null) }
    var statusNotificacao by remember { mutableStateOf("Não solicitada") }
    var exibirDialogExplicativo by remember { mutableStateOf(false) }

    // Launcher 1: Captura de Foto pela Câmera Nativa (sem salvar arquivo em disco)
    val cameraPreviewLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.TakePicturePreview()
    ) { bitmap: Bitmap? ->
        if (bitmap != null) {
            fotoCapturada = bitmap
        }
    }

    // Launcher 2: Solicitação da Permissão de Câmera
    val requisitarPermissaoCameraLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission()
    ) { concedida: Boolean ->
        if (concedida) {
            // Se a permissão acabou de ser concedida, abre a câmera imediatamente
            cameraPreviewLauncher.launch()
        } else {
            exibirDialogExplicativo = true
        }
    }

    // Launcher 3: Solicitação da Permissão de Notificações (Android 13+ / API 33)
    val notificacaoLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission()
    ) { concedida: Boolean ->
        statusNotificacao = if (concedida) "Permissão Concedida ✅" else "Permissão Negada ❌"
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Recursos Nativos & Permissões") },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = MaterialTheme.colorScheme.primaryContainer,
                    titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
                )
            )
        }
    ) { paddingVal ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingVal)
                .padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            // Card 1: Permissão de Notificação
            Card(
                modifier = Modifier.fillMaxWidth(),
                elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
            ) {
                Column(modifier = Modifier.padding(16.dp)) {
                    Text(
                        text = "1. Permissão de Notificações (Android 13+)",
                        style = MaterialTheme.typography.titleMedium,
                        fontWeight = FontWeight.Bold
                    )
                    Spacer(modifier = Modifier.height(4.dp))
                    Text(
                        text = "Status Atual: $statusNotificacao",
                        style = MaterialTheme.typography.bodyMedium,
                        color = MaterialTheme.colorScheme.secondary
                    )
                    Spacer(modifier = Modifier.height(8.dp))
                    Button(
                        onClick = {
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                                when {
                                    verificarPermissao(context, Manifest.permission.POST_NOTIFICATIONS) -> {
                                        statusNotificacao = "Permissão Já Estava Ativa ✅"
                                    }
                                    else -> {
                                        notificacaoLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
                                    }
                                }
                            } else {
                                statusNotificacao = "Não Requerida nesta Versão do Android (<= 12)"
                            }
                        },
                        modifier = Modifier.fillMaxWidth()
                    ) {
                        Icon(Icons.Default.Notifications, contentDescription = null)
                        Spacer(modifier = Modifier.size(8.dp))
                        Text("Verificar / Solicitar Notificação")
                    }
                }
            }

            // Card 2: Acesso à Câmera e Captura de Imagem
            Card(
                modifier = Modifier.fillMaxWidth(),
                elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
            ) {
                Column(
                    modifier = Modifier.padding(16.dp),
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    Text(
                        text = "2. Acesso à Câmera de Hardware",
                        style = MaterialTheme.typography.titleMedium,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier.fillMaxWidth()
                    )
                    Spacer(modifier = Modifier.height(12.dp))

                    // Área de exibição do preview capturado
                    Box(
                        modifier = Modifier
                            .size(180.dp)
                            .clip(RoundedCornerShape(12.dp))
                            .background(Color.LightGray.copy(alpha = 0.3f))
                            .border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(12.dp)),
                        contentAlignment = Alignment.Center
                    ) {
                        if (fotoCapturada != null) {
                            Image(
                                bitmap = fotoCapturada!!.asImageBitmap(),
                                contentDescription = "Foto capturada pela câmera",
                                modifier = Modifier.fillMaxSize()
                            )
                        } else {
                            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                                Icon(
                                    Icons.Default.CameraAlt,
                                    contentDescription = null,
                                    tint = Color.Gray,
                                    modifier = Modifier.size(48.dp)
                                )
                                Text("Nenhuma foto capturada", style = MaterialTheme.typography.bodySmall, color = Color.Gray)
                            }
                        }
                    }

                    Spacer(modifier = Modifier.height(12.dp))

                    Button(
                        onClick = {
                            if (verificarPermissao(context, Manifest.permission.CAMERA)) {
                                // Permissão já garantida: dispara o app de câmera
                                cameraPreviewLauncher.launch()
                            } else {
                                // Solicita permissão em runtime
                                requisitarPermissaoCameraLauncher.launch(Manifest.permission.CAMERA)
                            }
                        },
                        modifier = Modifier.fillMaxWidth()
                    ) {
                        Icon(Icons.Default.CameraAlt, contentDescription = null)
                        Spacer(modifier = Modifier.size(8.dp))
                        Text("Tirar Foto com Câmera")
                    }
                }
            }
        }
    }

    // Modal de Rationale (explicação ao usuário caso negado)
    if (exibirDialogExplicativo) {
        AlertDialog(
            onDismissRequest = { exibirDialogExplicativo = false },
            icon = { Icon(Icons.Default.Warning, contentDescription = null, tint = MaterialTheme.colorScheme.error) },
            title = { Text("Permissão Necessária") },
            text = {
                Text("O aplicativo precisa de acesso à câmera de hardware para anexar comprovantes aos lançamentos cadastrados. Por favor, autorize nas configurações do dispositivo.")
            },
            confirmButton = {
                TextButton(onClick = { exibirDialogExplicativo = false }) {
                    Text("Entendido")
                }
            }
        )
    }
}

// =====================================================================
// 2. Função Utilitária para Checagem Segura de Permissão
// =====================================================================

fun verificarPermissao(context: Context, permissao: String): Boolean {
    return ContextCompat.checkSelfPermission(
        context,
        permissao
    ) == PackageManager.PERMISSION_GRANTED
}

📌 Guia de Execução e Configuração no AndroidManifest.xml

Para que as permissões operem sem exceções de segurança (SecurityException):

  1. No arquivo app/src/main/AndroidManifest.xml, declare as permissões necessárias:
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-feature android:name="android.hardware.camera" android:required="false" />
    
  2. Nunca invoque APIs de hardware diretamente sem checar ContextCompat.checkSelfPermission previamente.
  3. Se o usuário negar a permissão duas vezes com a opção “Não perguntar novamente”, direcione-o via Intent explícita para Settings.ACTION_APPLICATION_DETAILS_SETTINGS.