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.FragmentChecando 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:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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ção | Resultado | O que fazer |
|---|---|---|
| Primeira vez pedindo | Rationale geralmente false | Você pode pedir diretamente (ou mostrar explicação curta antes) |
| Usuário negou (sem marcar “não perguntar”) | Rationale true | Mostre 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 negada | Nã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) eACCESS_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.GetContentou 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)?