📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.
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"!
| 1. Central de Comando GCPD | 2. Alarme Armado (RTC_WAKEUP) | 3. Disparo do Bat-Sinal |
|---|---|---|
| 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. |
Antes de iniciar este laboratório, certifique-se de ter compreendido:
Ao concluir este projeto autoguiado, você será capaz de:
BroadcastReceiver Personalizado: Criar receptores desacoplados que processam eventos disparados pelo sistema ou alarmes.AlarmManager: Utilizar setExactAndAllowWhileIdle() com a flag AlarmManager.RTC_WAKEUP para garantir a execução mesmo com a tela apagada.android:exported="false" no AndroidManifest.xml para blindar o receiver contra injeção maliciosa de terceiros.PendingIntent com FLAG_IMMUTABLE: Seguir os padrões modernos de segurança do Android 12+ ao despachar transmissões para o sistema operacional.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]
[!IMPORTANT] O perigo do
android:exported="true"em BroadcastReceivers: Se umBroadcastReceiverfor declarado semexported="false"ou com intent-filters abertos, qualquer aplicativo instalado no celular pode enviar umaIntentexplí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:
- Componente Fechado (
android:exported="false"): OGothamAlertReceiversó aceita transmissões enviadas internamente pelo nosso próprio aplicativo ou peloAlarmManagerdo sistema.- PendingIntent Imutável (
FLAG_IMMUTABLE): Impede que interceptadores modifiquem os extras (vilao_nome,localizacao) contidos no envelope do alarme.- Conformidade Android 13+: Permissão
POST_NOTIFICATIONSsolicitada em tempo de execução e permissão declarativaSCHEDULE_EXACT_ALARM.- Higiene do Repositório: Arquivo
.gitignoreconfigurado para evitar o versionamento de segredos e dados transitórios de compilação.
| 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. |
File > New > New Project).Alerta Gotham (ou android_p16_gotham_broadcast)br.com.curso.gothamKotlinAPI 24 ("Nougat"; Android 7.0) ou superiorGroovy DSL (build.gradle) ou Kotlin DSL (build.gradle.kts)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'
}
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>
GothamAlertReceiverAbra 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())
}
}
}
}
AlarmManagerfun 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
)
}
}
@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)
}
}
| 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(...). |
alarmManager.cancel(pendingIntent).setRepeating() para enviar relatórios da patrulha do Batman a cada intervalo.NotificationChannel para tocar junto com a notificação.Disponível em: app/src/main/java/br/com/curso/gotham/MainActivity.kt.
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
./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.