📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.

🚨 P16: Alerta Gotham (Broadcast Receivers & AlarmManager)

Bem-vindo à quinta etapa da Fase 7: Arquitetura Avançada & Comunicação Inter-Processos (IPC)! 🚨

Neste projeto prático, você aprenderá a construir rotinas de agendamento preciso no Android através do AlarmManager combinadas com a escuta segura de eventos do sistema via BroadcastReceiver.

Com a temática sombria do departamento de polícia de Gotham City (GCPD) e do icônico Bat-Sinal, criaremos um sistema onde o Comissário Gordon agenda alertas de emergência para horários exatos. Você aprenderá como acordar o processador do celular mesmo em modo de economia profunda (Doze Mode via RTC_WAKEUP), solicitar a permissão do Android 13+ (POST_NOTIFICATIONS) e, acima de tudo, blindar o aplicativo contra ataques de Intent Spoofing utilizando o atributo de segurança android:exported="false"!

Fluxo Completo de Telas do Alerta Gotham


📱 Galeria de Telas da Aplicação

1. Central de Comando GCPD 2. Alarme Armado (RTC_WAKEUP) 3. Disparo do Bat-Sinal
Tela 1 - Painel GCPD Tela 2 - Alarme Armado Tela 3 - Notificação GCPD
Seleção de tempo (5s, 10s, 30s) e alerta de vilão. Agendamento com contagem regressiva e modo Doze. Notificação Heads-Up disparada pelo BroadcastReceiver.

✅ Pré-requisitos e Continuidade

Antes de iniciar este laboratório, certifique-se de ter compreendido:


🎯 Objetivos de Aprendizagem

Ao concluir este projeto autoguiado, você será capaz de:

  1. Implementar um BroadcastReceiver Personalizado: Criar receptores desacoplados que processam eventos disparados pelo sistema ou alarmes.
  2. Agendar Alarmes Exatos com AlarmManager: Utilizar setExactAndAllowWhileIdle() com a flag AlarmManager.RTC_WAKEUP para garantir a execução mesmo com a tela apagada.
  3. Proteger Componentes contra Intent Spoofing: Configurar android:exported="false" no AndroidManifest.xml para blindar o receiver contra injeção maliciosa de terceiros.
  4. Utilizar PendingIntent com FLAG_IMMUTABLE: Seguir os padrões modernos de segurança do Android 12+ ao despachar transmissões para o sistema operacional.
  5. Integrar Notificações Heads-Up no Broadcast: Emitir alertas com som e vibração de alta prioridade quando o alarme expirar.

🏗️ Ciclo de Disparo e Segurança do Alarme

graph TD
    A[Usuário seleciona vilão, tempo e clica em PROJETAR BAT-SINAL] --> B[AlarmManager obtém PendingIntent.getBroadcast]
    B --> C[setExactAndAllowWhileIdle agenda alarme com RTC_WAKEUP]
    C --> D[Usuário bloqueia a tela do celular / entra em Doze Mode]
    D --> E[Relógio do Hardware atinge o tempo exato programado]
    E --> F[SO acorda a CPU e despacha Intent para GothamAlertReceiver]
    F --> G{Receiver possui exported=false?}
    G -- Sim --> H[Apenas o próprio app e o SO podem disparar o evento - SEGURO!]
    H --> I[onReceive executa e monta NotificationCompat.Builder]
    I --> J[NotificationManagerCompat.notify exibe alerta na barra de status]

🛡️ Boas Práticas de Segurança em IPC e Broadcasts (OWASP Mobile M1/M6)

[!IMPORTANT] O perigo do android:exported="true" em BroadcastReceivers: Se um BroadcastReceiver for declarado sem exported="false" ou com intent-filters abertos, qualquer aplicativo instalado no celular pode enviar uma Intent explícita forjando dados falsos (ex: fingindo que um alarme crítico da GCPD foi disparado ou disparando transações não autorizadas).

Nossa Blindagem de Segurança:

  1. Componente Fechado (android:exported="false"): O GothamAlertReceiver só aceita transmissões enviadas internamente pelo nosso próprio aplicativo ou pelo AlarmManager do sistema.
  2. PendingIntent Imutável (FLAG_IMMUTABLE): Impede que interceptadores modifiquem os extras (vilao_nome, localizacao) contidos no envelope do alarme.
  3. Conformidade Android 13+: Permissão POST_NOTIFICATIONS solicitada em tempo de execução e permissão declarativa SCHEDULE_EXACT_ALARM.
  4. Higiene do Repositório: Arquivo .gitignore configurado para evitar o versionamento de segredos e dados transitórios de compilação.

📖 Dicionário Técnico do Projeto

Termo / Componente O que é e para que serve?
BroadcastReceiver Componente fundamental do Android que atua como ouvinte de mensagens e anúncios assíncronos do sistema operacional.
AlarmManager Serviço de baixo nível do Android que permite agendar a execução de código em um momento futuro exato do relógio em tempo real.
RTC_WAKEUP Tipo de alarme baseado no relógio de tempo real (Real-Time Clock) que acorda o processador do celular se o aparelho estiver dormindo.
android:exported=”false” Atributo do manifesto que restringe o acesso ao componente exclusivamente ao próprio aplicativo e ao sistema operacional.
PendingIntent.getBroadcast Criação de um token seguro de execução futura delegado ao AlarmManager.
SCHEDULE_EXACT_ALARM Permissão do Android para agendamento de alarmes que exigem precisão de segundos.

🛠️ Passo a Passo de Implementação

🚀 Passo 1: Criando o Novo Projeto no Android Studio

  1. Abra o Android Studio e clique em New Project (ou File > New > New Project).
  2. Selecione o template Empty Activity (com o ícone do Jetpack Compose 🌌).
  3. Preencha as configurações do projeto:
    • Name: Alerta Gotham (ou android_p16_gotham_broadcast)
    • Package name: br.com.curso.gotham
    • Save location: Pasta do seu projeto no repositório
    • Language: Kotlin
    • Minimum SDK: API 24 ("Nougat"; Android 7.0) ou superior
    • Build configuration language: Groovy DSL (build.gradle) ou Kotlin DSL (build.gradle.kts)
  4. Clique em Finish e aguarde o Gradle sincronizar.

📦 Passo 2: Configurando Dependências no Gradle (build.gradle)

Abra app > build.gradle:

dependencies {
    implementation platform('androidx.compose:compose-bom:2024.02.00')
    implementation 'androidx.compose.ui:ui'
    implementation 'androidx.compose.ui:ui-graphics'
    implementation 'androidx.compose.ui:ui-tooling-preview'
    implementation 'androidx.compose.material3:material3'
    implementation 'androidx.activity:activity-compose:1.8.2'
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
}

📄 Passo 3: Declarando Permissões e o Receiver Seguro no Manifesto (AndroidManifest.xml)

Abra app > src > main > AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- Permissões de Notificação e Agendamento Preciso -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
    <uses-permission android:name="android.permission.VIBRATE" />

    <application
        android:allowBackup="true"
        android:label="Alerta Gotham"
        android:supportsRtl="true"
        android:theme="@style/Theme.CartaoTreinador">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!-- SEGURANÇA OWASP: exported="false" impede que outros apps maliciosos injetem Intents no Receiver -->
        <receiver
            android:name=".GothamAlertReceiver"
            android:exported="false" />
    </application>

</manifest>

🚨 Passo 4: Implementando o GothamAlertReceiver

Abra app > src > main > java > br > com > curso > gotham > MainActivity.kt:

const val CANAL_GCPD_ID = "GCPD_ALERTS"

class GothamAlertReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val vilao = intent.getStringExtra("vilao_nome") ?: "Coringa"
        val localizacao = intent.getStringExtra("localizacao") ?: "Asilo Arkham"

        val notificationIntent = Intent(context, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }

        // FLAG_IMMUTABLE protege contra ataques de mutação de intent
        val pendingIntent = PendingIntent.getActivity(
            context,
            101,
            notificationIntent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )

        val builder = NotificationCompat.Builder(context, CANAL_GCPD_ID)
            .setSmallIcon(android.R.drawable.ic_dialog_alert)
            .setContentTitle("🚨 ALERTA MÁXIMO DA GCPD!")
            .setContentText("O Comissário Gordon avistou $vilao em $localizacao! Vá para a cobertura!")
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)

        with(NotificationManagerCompat.from(context)) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
                ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
            ) {
                notify(101, builder.build())
            }
        }
    }
}

⏰ Passo 5: Agendando o Alarme com o AlarmManager

fun agendarBatSinal(context: Context, segundos: Int, vilao: String) {
    val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
    val intent = Intent(context, GothamAlertReceiver::class.java).apply {
        putExtra("vilao_nome", vilao)
        putExtra("localizacao", "Ruas de Gotham")
    }

    val pendingIntent = PendingIntent.getBroadcast(
        context,
        101,
        intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
    )

    val triggerTime = System.currentTimeMillis() + (segundos * 1000L)

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        alarmManager.setExactAndAllowWhileIdle(
            AlarmManager.RTC_WAKEUP,
            triggerTime,
            pendingIntent
        )
    } else {
        alarmManager.setExact(
            AlarmManager.RTC_WAKEUP,
            triggerTime,
            pendingIntent
        )
    }
}

🦇 Passo 6: Interface Gotham no Compose com Verificação de Permissão

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GothamAlarmScreen() {
    val context = LocalContext.current
    var alarmStatus by remember { mutableStateOf("Bat-Sinal Inativo") }
    var tempoSelecionadoSegundos by remember { mutableIntStateOf(5) }
    var vilaoSelecionado by remember { mutableStateOf("Coringa (Fuga de Arkham)") }

    val launcherPermissao = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission()
    ) { concedida ->
        if (concedida) Toast.makeText(context, "Permissão da GCPD Concedida!", Toast.LENGTH_SHORT).show()
    }

    // Botão de Disparo do Alarme
    Button(
        onClick = {
            agendarBatSinal(context, tempoSelecionadoSegundos, vilaoSelecionado)
            alarmStatus = "Bat-Sinal agendado para daqui a ${tempoSelecionadoSegundos}s! Pode bloquear a tela." }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE53935)) ) { Text("PROJETAR BAT-SINAL NAS NUVENS (${tempoSelecionadoSegundos}S)", color = Color.White, fontWeight = FontWeight.Bold)
    }
}

🔍 Guia de Diagnóstico & Resolução de Problemas (Troubleshooting)

Sintoma Observado Causa Provável Como Resolver
O alarme dispara, mas nenhuma notificação aparece na tela No Android 13+ (API 33+), a permissão POST_NOTIFICATIONS não foi concedida pelo usuário. Solicite a permissão em runtime usando rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()).
SecurityException: Caller does not have SCHEDULE_EXACT_ALARM permission Falta da permissão declarativa de alarmes exatos no manifesto. Adicione <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> no AndroidManifest.xml.
O alarme atrasa vários minutos quando a tela do celular é bloqueada Uso de set() comum em vez de setExactAndAllowWhileIdle(). O Doze Mode agrupa os alarmes normais para economizar bateria. Utilize alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, ...) para forçar a execução no segundo exato.
IllegalArgumentException: Targeting S+ requires FLAG_IMMUTABLE O PendingIntent foi instanciado sem a flag de segurança exigida no Android 12+. Adicione PendingIntent.FLAG_IMMUTABLE no método PendingIntent.getBroadcast(...).

🏆 Desafios e Upgrades (Mão na Massa!)

  1. 📴 Cancelamento do Bat-Sinal: Crie um botão “CANCELAR ALARME” que invoque alarmManager.cancel(pendingIntent).
  2. 🔄 Alarme Repetitivo de Patrulha: Implemente um alarme periódico com setRepeating() para enviar relatórios da patrulha do Batman a cada intervalo.
  3. 🔊 Som da Sirene da GCPD: Inclua um arquivo de áudio de sirene no NotificationChannel para tocar junto com a notificação.

📖 Gabarito Oficial de Código (Para Conferência)

Disponível em: app/src/main/java/br/com/curso/gotham/MainActivity.kt.


🚀 Como Executar no Laboratório

1. Abra o terminal na pasta deste projeto

No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:

cd proj_aplicacoes_full_stack/projetos/android_p16_gotham_broadcast

2. Execute a aplicação e os testes

./gradlew build
./gradlew test
# ou abrir no Android Studio e clicar em Run (Shift+F10)

[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code ou Android Studio, abra o projeto diretamente pela pasta proj_aplicacoes_full_stack/projetos/android_p16_gotham_broadcast.