📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.
Bem-vindo à terceira etapa da Fase 7: Arquitetura Avançada & Comunicação com o Usuário! 🌀
Neste projeto de alto impacto, você combinará dois pilares fundamentais do desenvolvimento Android moderno: o roteamento declarativo com Navigation Compose (passagem tipada de argumentos via URL) e o sistema nativo de Notificações Push Locais utilizando NotificationManager, NotificationChannel e o modelo de permissão de segurança do Android 13+ (POST_NOTIFICATIONS).
Com a temática de ficção científica e viagens interdimensionais inspirada em Rick and Morty, criaremos um aplicativo onde o usuário navega entre mundos paralelos, acessa fendas específicas e dispara alertas com PendingIntent Imutável Seguro diretamente na barra de status do smartphone!
| 1. Catálogo de Portais | 2. Fenda Dimensional ({id}) | 3. Notificação Push Nativa |
|---|---|---|
| Mapeamento de mundos com status de notificação. | Ficha da dimensão com botão de emissão de alerta. | Banner Heads-Up nativo com PendingIntent imutável. |
Antes de iniciar este laboratório, certifique-se de ter compreendido:
Ao concluir este projeto autoguiado, você será capaz de:
lista_dimensoes) e dinâmicas (detalhe_dimensao/{dimensaoId}) com NavHost e rememberNavController().NotificationChannel): Criar canais dedicados com níveis de importância (IMPORTANCE_HIGH), som e vibração no Android 8.0+ (API 26+).POST_NOTIFICATIONS em Tempo de Execução: Implementar o fluxo moderno de permissão no Android 13+ (API 33+) com rememberLauncherForActivityResult.PendingIntent.FLAG_IMMUTABLE contra ataques de sequestro e mutação de Intents.NotificationCompat.Builder: Configurar ícone, título, texto de alerta e intenção de clique para reabrir o app de forma limpa.graph TD
A[MainActivity: inicializa NotificationChannel] --> B[rememberNavController]
B --> C[NavHost: registra as rotas]
C --> D[Rota: lista_dimensoes - Tela Inicial]
D --> E{Permissão POST_NOTIFICATIONS concedida?}
E -- Não (Android 13+) --> F[Exibe botão para disparar Launcher de Permissão]
E -- Sim --> G[Status: Canal Push Ativo e Seguro]
D -->|Clique no Card de Dimensão| H["Rota: detalhe_dimensao/{dimensaoId}"]
H --> I[Renderiza Ficha da Dimensão com Perigo e Coordenadas]
I -->|Botão EMITIR NOTIFICAÇÃO| J[dispararNotificacaoPortal]
J --> K[PendingIntent com FLAG_IMMUTABLE]
K --> L[NotificationCompat.Builder -> notify]
L --> M[Banner de Notificação Heads-Up na Barra de Status]
I -->|TopAppBar Seta Voltar| N[popBackStack -> Retorna para lista_dimensoes]
[!IMPORTANT] Vulnerabilidade de Intent Mutation em Notificações: Versões antigas do Android permitiam a criação de
PendingIntentmutáveis por padrão (FLAG_MUTABLE). Aplicativos maliciosos instalados no mesmo dispositivo podiam interceptar essa intenção e alterar os parâmetros de destino (Intent Hijacking).Como protegemos nosso app:
- Uso de
PendingIntent.FLAG_IMMUTABLE: Garante que a intenção disparada pela notificação não possa ser interceptada ou alterada por processos externos.- Consentimento Explícito (
POST_NOTIFICATIONS): Não disparamos notificações sem autorização prévia do usuário no Android 13+.- Higiene de Versionamento: O arquivo
.gitignoreisola caches e dados temporários do projeto.
| Termo / Componente | O que é e para que serve? |
|---|---|
| NotificationManager | Serviço de sistema do Android responsável por publicar, atualizar e cancelar notificações na bandeja do sistema. |
| NotificationChannel | Canal obrigatório desde o Android 8.0 (API 26) que agrupa notificações por categoria e permite ao usuário configurar alertas individualmente. |
| POST_NOTIFICATIONS | Permissão de tempo de execução introduzida no Android 13 (API 33) necessária para exibir notificações na tela. |
| PendingIntent | Um token concedido ao sistema operacional Android permitindo que ele execute uma ação futura em nome do seu aplicativo (ex: ao tocar na notificação). |
| FLAG_IMMUTABLE | Flag de segurança que impede que outros apps alterem o conteúdo do PendingIntent antes de sua execução. |
| Navigation Compose | Biblioteca do Jetpack Compose para navegação com rotas, backstack e passagem de parâmetros. |
File > New > New Project).Portais Dimensionais (ou android_p14_portais_push)br.com.curso.portaisKotlinAPI 24 ("Nougat"; Android 7.0) ou superiorGroovy DSL (build.gradle) ou Kotlin DSL (build.gradle.kts)build.gradle)Abra app > build.gradle e adicione a dependência do Navigation Compose:
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'
// Navigation Compose
implementation 'androidx.navigation:navigation-compose:2.7.6'
}
Clique em Sync Now.
AndroidManifest.xml)Abra app > src > main > AndroidManifest.xml e declare as permissões de notificação e vibração:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissão para disparo de notificações no Android 13+ (API 33+) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:allowBackup="true"
android:label="Portais Dimensionais"
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>
</application>
</manifest>
Dimensao)Abra app > src > main > java > br > com > curso > portais > MainActivity.kt:
data class Dimensao(
val id: String,
val name: String,
val dangerLevel: String,
val description: String,
val emoji: String,
val coordenadas: String
)
val dimensoesList = listOf(
Dimensao("C137", "Terra C-137", "MÁXIMO", "Dimensão natal de Rick Sanchez C-137. Totalmente devastada por Cronenbergs.", "☣️", "Setor Espacial 4-A"),
Dimensao("J197", "Dimensão Doce", "BAIXO", "Onde a biosfera inteira é feita de caramelo e lagos de chocolate.", "🍭", "Quadrante Açúcar 7"),
Dimensao("F345", "Dimensão dos Sofás", "MÉDIO", "Onde as pessoas são sofás e os sofás são pessoas.", "🛋️", "Faixa Möbius 12"),
Dimensao("R777", "Cidadela dos Ricks", "ALTO", "Fortaleza interdimensional governada pelo Conselho dos Ricks.", "🏛️", "Nexo Central")
)
const val CHANNEL_ID = "canal_portais_dimensionais"
fun criarCanalDeNotificacao(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val canal = NotificationChannel(
CHANNEL_ID,
"Alertas de Portais Dimensionais",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notifica quando um novo portal interdimensional for aberto"
enableVibration(true)
}
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(canal)
}
}
fun dispararNotificacaoPortal(context: Context, dimensao: Dimensao) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
// FLAG_IMMUTABLE: Garante segurança contra adulteração de Intent
val pendingIntent = PendingIntent.getActivity(
context,
dimensao.id.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle("🌀 Portal Conectado: ${dimensao.name}") .setContentText("Fenda aberta para${dimensao.id} (Perigo: ${dimensao.dangerLevel})")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(dimensao.id.hashCode(), notification)
}
@Composable
fun PortaisAppNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "lista_dimensoes") {
composable("lista_dimensoes") {
TelaListaDimensoes(
onDimensaoClick = { id -> navController.navigate("detalhe_dimensao/$id") }
)
}
composable(
route = "detalhe_dimensao/{dimensaoId}",
arguments = listOf(navArgument("dimensaoId") { type = NavType.StringType })
) { backStackEntry ->
val dimensaoId = backStackEntry.arguments?.getString("dimensaoId") ?: ""
TelaDetalheDimensao(
dimensaoId = dimensaoId,
onVoltarClick = { navController.popBackStack() }
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaListaDimensoes(onDimensaoClick: (String) -> Unit) {
val context = LocalContext.current
var temPermissaoNotificacao by remember {
mutableStateOf(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
} else true
)
}
val launcherPermissao = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { concedida ->
temPermissaoNotificacao = concedida
}
Scaffold(
containerColor = MaterialTheme.colorScheme.background,
topBar = {
CenterAlignedTopAppBar(
title = { Text("PORTAIS INTERDIMENSIONAIS", fontWeight = FontWeight.Black, letterSpacing = 2.sp) },
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color(0xFF161D26), titleContentColor = Color(0xFF00E676))
)
}
) { padding ->
LazyColumn(modifier = Modifier.padding(padding).fillMaxSize().padding(horizontal = 16.dp)) {
// Card de status da permissão e lista de dimensões clicáveis
items(dimensoesList, key = { it.id }) { dimensao ->
Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp).clickable { onDimensaoClick(dimensao.id) },
colors = CardDefaults.cardColors(containerColor = Color(0xFF161D26)),
shape = RoundedCornerShape(14.dp)
) {
Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
Text(dimensao.emoji, fontSize = 32.sp)
Spacer(modifier = Modifier.width(14.dp))
Column(modifier = Modifier.weight(1f)) {
Text(dimensao.name, fontWeight = FontWeight.Bold, color = Color.White, fontSize = 15.sp)
Text("Código: ${dimensao.id}", color = Color.Gray, fontSize = 12.sp)
}
Text("ENTRAR 🌀", color = Color(0xFF00E676), fontWeight = FontWeight.Bold)
}
}
}
}
}
}
| Sintoma Observado | Causa Provável | Como Resolver |
|---|---|---|
| A notificação não aparece no Android 13+ (API 33+) | A permissão POST_NOTIFICATIONS não foi solicitada em tempo de execução. |
Utilize o rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) para solicitar a autorização antes de chamar notify(). |
| A notificação não toca som nem vibra | O canal de notificação foi criado com importância baixa ou sem vibração ativada. | Defina NotificationManager.IMPORTANCE_HIGH e adicione enableVibration(true) ao configurar o NotificationChannel. |
IllegalArgumentException: Targeting S+ requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified |
O PendingIntent foi criado sem especificar a flag de imutabilidade exigida a partir do Android 12. |
Adicione PendingIntent.FLAG_IMMUTABLE na chamada PendingIntent.getActivity(...). |
| Ao clicar na notificação, nada acontece | O setContentIntent(pendingIntent) não foi anexado ao NotificationCompat.Builder. |
Adicione .setContentIntent(pendingIntent) e .setAutoCancel(true). |
.addAction().NotificationCompat.BigTextStyle()..mp3 de efeito sonoro de ficção científica da pasta res/raw ao canal de notificação.Disponível em: app/src/main/java/br/com/curso/portais/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_p14_portais_push
./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_p14_portais_push.