Permissões Android: runtime permissions, casos comuns e UX responsável

Capítulo 10

Tempo estimado de leitura: 9 minutos

+ Exercício

O que são permissões e por que existem “runtime permissions”

No Android, permissões controlam o acesso do app a recursos sensíveis (ex.: localização, câmera, microfone, contatos). Desde o Android 6.0 (API 23), várias permissões passaram a ser concedidas em tempo de execução (runtime): o app declara no manifesto, mas o usuário decide durante o uso, no momento em que a funcionalidade realmente precisa do acesso.

Isso muda a forma correta de implementar: em vez de pedir tudo ao abrir o app, você deve pedir apenas quando necessário, explicar o motivo e lidar com recusas sem quebrar a experiência.

Tipos comuns de permissões (visão prática)

  • Normal: concedidas automaticamente (ex.: acesso à internet). Não exigem diálogo de runtime.
  • Dangerous: exigem runtime (ex.: localização, câmera, microfone).
  • Permissões “especiais”: não seguem o fluxo padrão (ex.: sobrepor apps, acesso a notificações, “All files access”). Geralmente exigem levar o usuário a uma tela de configurações específica.

Princípios de UX responsável ao pedir permissão

  • Peça no contexto: quando o usuário toca em “Tirar foto”, aí sim peça Câmera.
  • Explique antes do diálogo (pre-permission rationale): uma mensagem curta dizendo o benefício e o que acontece se negar.
  • Tenha alternativa: se negar, ofereça um caminho que não dependa da permissão (ex.: escolher imagem da galeria em vez de câmera, ou digitar endereço em vez de localização).
  • Não bloqueie a navegação: evite telas “travadas” exigindo permissão para usar o app inteiro.
  • Respeite a decisão: se o usuário negar, não fique pedindo em loop.

Como solicitar permissões com Activity Result APIs

O jeito moderno é usar ActivityResultContracts. Você registra um launcher e, quando precisar, chama launch(). Isso evita APIs antigas e torna o fluxo mais previsível.

Dependências e imports

Em geral, você já terá AndroidX Activity/Fragment. O essencial é usar:

import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment

Checando se já está concedida

private fun hasPermission(permission: String): Boolean {
    return ContextCompat.checkSelfPermission(
        requireContext(),
        permission
    ) == PackageManager.PERMISSION_GRANTED
}

Solicitando uma permissão única

Exemplo com Câmera:

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

private val requestCameraPermission = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { granted: Boolean ->
    if (granted) {
        // Pode usar a câmera
    } else {
        // Usuário negou
    }
}

Para disparar:

requestCameraPermission.launch(Manifest.permission.CAMERA)

Solicitando múltiplas permissões

Útil para casos como localização aproximada + precisa:

private val requestLocationPermissions = registerForActivityResult(
    ActivityResultContracts.RequestMultiplePermissions()
) { result: Map<String, Boolean> ->
    val fine = result[Manifest.permission.ACCESS_FINE_LOCATION] == true
    val coarse = result[Manifest.permission.ACCESS_COARSE_LOCATION] == true

    // Trate combinações: fine/coarse/nenhuma
}

Rationale, negação e “não perguntar novamente”

Quando o usuário nega, o Android pode indicar se você deve mostrar uma explicação antes de pedir de novo, via shouldShowRequestPermissionRationale(permission).

Como interpretar os estados

SituaçãoResultadoO que fazer
Primeira vez pedindoRationale geralmente falseVocê pode pedir diretamente (ou mostrar explicação curta antes)
Usuário negou (sem marcar “não perguntar”)Rationale trueMostre explicação e ofereça tentar novamente
Usuário negou e marcou “não perguntar novamente” (ou política do sistema)Rationale false e permissão negadaNão adianta pedir de novo; oriente a abrir Configurações do app

Abrindo a tela de configurações do app

Quando o usuário bloqueou permanentemente, você deve oferecer um botão “Abrir configurações”.

import android.content.Intent
import android.net.Uri
import android.provider.Settings

private fun openAppSettings() {
    val intent = Intent(
        Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
        Uri.fromParts("package", requireContext().packageName, null)
    )
    startActivity(intent)
}

Casos típicos e diferenças em versões recentes

Câmera

  • Permissão: Manifest.permission.CAMERA
  • Boa prática: peça somente ao tocar em “Tirar foto”. Se negar, permita escolher uma imagem existente (sem câmera).

Localização (aproximada vs precisa)

  • Permissões: ACCESS_COARSE_LOCATION (aproximada) e ACCESS_FINE_LOCATION (precisa).
  • O usuário pode conceder apenas aproximada. Sua UI deve refletir isso (ex.: “Mostrando região aproximada”).
  • Em versões recentes, o sistema dá mais controle ao usuário; não assuma que “localização concedida” significa “precisa”.

Armazenamento / fotos e mídia (mudanças importantes)

O acesso a arquivos mudou bastante. Em vez de pedir permissões amplas, prefira APIs que não exigem permissão (como o seletor do sistema).

  • Android 13+ (API 33): permissões separadas para mídia, como READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO.
  • Android 12 e anteriores: era comum READ_EXTERNAL_STORAGE (hoje desaconselhada em muitos cenários).
  • Recomendação: para escolher uma foto, use o seletor do sistema (ActivityResultContracts.GetContent ou Photo Picker quando disponível), evitando pedir permissão de leitura.

Prática: funcionalidade simples com estados de consentimento na UI (Câmera)

Vamos implementar uma tela que:

  • Mostra o estado atual da permissão de câmera
  • Permite pedir permissão
  • Se negada, mostra orientação
  • Se bloqueada (“não perguntar novamente”), oferece botão para Configurações

1) Declare a permissão no AndroidManifest

<manifest ...>
    <uses-permission android:name="android.permission.CAMERA" />

    <application ...>
        ...
    </application>
</manifest>

2) Modele os estados de permissão

sealed class CameraPermissionState {
    data object Granted : CameraPermissionState()
    data object DeniedCanAskAgain : CameraPermissionState()
    data object DeniedPermanently : CameraPermissionState()
    data object NotRequestedYet : CameraPermissionState()
}

3) Crie um Fragment com UI simples (Views)

Exemplo de layout (apenas para referência): um texto de status e botões.

<LinearLayout ... android:orientation="vertical">

    <TextView
        android:id="@+id/tvStatus"
        ... />

    <Button
        android:id="@+id/btnRequest"
        android:text="Permitir câmera"
        ... />

    <Button
        android:id="@+id/btnOpenSettings"
        android:text="Abrir configurações"
        android:visibility="gone"
        ... />

</LinearLayout>

4) Implemente a lógica de estado + launcher

class CameraPermissionFragment : Fragment(R.layout.fragment_camera_permission) {

    private lateinit var tvStatus: TextView
    private lateinit var btnRequest: Button
    private lateinit var btnOpenSettings: Button

    private val requestCameraPermission = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->
        render(getCameraPermissionState())
        if (granted) {
            // Aqui você chamaria a funcionalidade que depende da câmera
            // Ex.: abrir uma tela de captura
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        tvStatus = view.findViewById(R.id.tvStatus)
        btnRequest = view.findViewById(R.id.btnRequest)
        btnOpenSettings = view.findViewById(R.id.btnOpenSettings)

        btnRequest.setOnClickListener {
            val state = getCameraPermissionState()
            when (state) {
                CameraPermissionState.Granted -> {
                    // Já tem permissão
                }
                CameraPermissionState.DeniedPermanently -> {
                    // Não adianta pedir; guie para configurações
                    openAppSettings()
                }
                CameraPermissionState.DeniedCanAskAgain,
                CameraPermissionState.NotRequestedYet -> {
                    // Opcional: mostrar uma explicação antes
                    requestCameraPermission.launch(Manifest.permission.CAMERA)
                }
            }
        }

        btnOpenSettings.setOnClickListener {
            openAppSettings()
        }

        render(getCameraPermissionState())
    }

    override fun onResume() {
        super.onResume()
        // Importante: ao voltar das Configurações, atualize a UI
        render(getCameraPermissionState())
    }

    private fun getCameraPermissionState(): CameraPermissionState {
        val permission = Manifest.permission.CAMERA
        val granted = ContextCompat.checkSelfPermission(
            requireContext(),
            permission
        ) == PackageManager.PERMISSION_GRANTED

        if (granted) return CameraPermissionState.Granted

        val canShowRationale = shouldShowRequestPermissionRationale(permission)

        // Heurística prática:
        // - Se não está concedida e o sistema diz para mostrar rationale, o usuário negou antes e dá para pedir de novo.
        // - Se não está concedida e rationale é false, pode ser primeira vez OU bloqueio permanente.
        // Para diferenciar "primeira vez" de "bloqueio permanente", você pode persistir um flag local.
        val prefs = requireContext().getSharedPreferences("perm_prefs", 0)
        val askedBefore = prefs.getBoolean("asked_camera", false)

        return when {
            canShowRationale -> CameraPermissionState.DeniedCanAskAgain
            !askedBefore -> CameraPermissionState.NotRequestedYet
            else -> CameraPermissionState.DeniedPermanently
        }
    }

    private fun render(state: CameraPermissionState) {
        when (state) {
            CameraPermissionState.Granted -> {
                tvStatus.text = "Câmera: permitida"
                btnOpenSettings.visibility = View.GONE
                btnRequest.text = "Permissão concedida"
                btnRequest.isEnabled = false
            }
            CameraPermissionState.NotRequestedYet -> {
                tvStatus.text = "Câmera: ainda não solicitada"
                btnOpenSettings.visibility = View.GONE
                btnRequest.text = "Permitir câmera"
                btnRequest.isEnabled = true
            }
            CameraPermissionState.DeniedCanAskAgain -> {
                tvStatus.text = "Câmera: negada. Podemos pedir novamente com uma explicação."
                btnOpenSettings.visibility = View.GONE
                btnRequest.text = "Tentar novamente"
                btnRequest.isEnabled = true
            }
            CameraPermissionState.DeniedPermanently -> {
                tvStatus.text = "Câmera: bloqueada. Ative nas configurações do app."
                btnOpenSettings.visibility = View.VISIBLE
                btnRequest.text = "Permitir câmera"
                btnRequest.isEnabled = true
            }
        }
    }

    private fun openAppSettings() {
        val intent = Intent(
            Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
            Uri.fromParts("package", requireContext().packageName, null)
        )
        startActivity(intent)
    }

    private fun markAskedCamera() {
        val prefs = requireContext().getSharedPreferences("perm_prefs", 0)
        prefs.edit().putBoolean("asked_camera", true).apply()
    }

    private fun requestCameraWithTracking() {
        markAskedCamera()
        requestCameraPermission.launch(Manifest.permission.CAMERA)
    }
}

Observação importante: no código acima, criamos markAskedCamera() e requestCameraWithTracking() para registrar que já perguntamos antes. Para usar corretamente, substitua a chamada direta do launcher por:

requestCameraWithTracking()

5) Ajuste de UX: explicação antes de pedir (rationale)

Quando DeniedCanAskAgain, mostre uma explicação curta (pode ser um diálogo) antes de chamar requestCameraWithTracking(). Exemplo simplificado:

private fun showCameraRationaleAndRequest() {
    AlertDialog.Builder(requireContext())
        .setTitle("Permissão de câmera")
        .setMessage("Usamos a câmera para você tirar uma foto do seu perfil. Sem isso, você ainda pode escolher uma imagem existente.")
        .setNegativeButton("Agora não", null)
        .setPositiveButton("Continuar") { _, _ ->
            requestCameraWithTracking()
        }
        .show()
}

Extra: escolhendo uma imagem sem pedir permissão de armazenamento

Para reduzir fricção, ofereça uma alternativa que não exija permissões amplas: o seletor do sistema.

private val pickImage = registerForActivityResult(
    ActivityResultContracts.GetContent()
) { uri: Uri? ->
    // Use o URI retornado (ex.: mostrar em um ImageView)
}

private fun pickFromGallery() {
    pickImage.launch("image/*")
}

Esse fluxo normalmente não exige pedir permissões de leitura de armazenamento, e funciona bem em versões recentes.

Checklist rápido para implementar permissões com segurança

  • Declarou no Manifest apenas o necessário?
  • Checa permissão antes de usar o recurso?
  • Pede no contexto (após ação do usuário)?
  • Mostra rationale quando apropriado?
  • Lida com negação sem quebrar a tela?
  • Detecta bloqueio permanente e oferece Configurações?
  • Atualiza a UI ao voltar das Configurações (onResume)?
  • Quando possível, usa alternativas sem permissão (seletor de mídia)?

Agora responda o exercício sobre o conteúdo:

Ao implementar uma funcionalidade que depende de câmera no Android (API 23+), qual abordagem melhor segue as boas práticas de runtime permissions e UX responsável?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

A prática recomendada é pedir permissões em contexto, explicar o motivo (rationale) quando necessário, não travar a navegação e oferecer alternativas. Se o usuário bloquear permanentemente, não adianta pedir de novo: é melhor guiar para as Configurações do app.

Próximo capitúlo

Persistência local com Room em Kotlin: entidades, DAO, migrations e testes

Arrow Right Icon
Capa do Ebook gratuito Android para Iniciantes com Kotlin: construindo seu primeiro app moderno
67%

Android para Iniciantes com Kotlin: construindo seu primeiro app moderno

Novo curso

15 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.