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

🌀 P14: Portais Dimensionais (Navigation Compose & Notificações Push)

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!

Fluxo Completo de Telas dos Portais Dimensionais


📱 Galeria de Telas da Aplicação

1. Catálogo de Portais 2. Fenda Dimensional ({id}) 3. Notificação Push Nativa
Tela 1 - Lista de Portais Tela 2 - Detalhe Fenda Tela 3 - Notificação Push
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.

✅ 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. Configurar o Navigation Compose: Declarar rotas estáticas (lista_dimensoes) e dinâmicas (detalhe_dimensao/{dimensaoId}) com NavHost e rememberNavController().
  2. Construir Canais de Notificação (NotificationChannel): Criar canais dedicados com níveis de importância (IMPORTANCE_HIGH), som e vibração no Android 8.0+ (API 26+).
  3. Solicitar a Permissão POST_NOTIFICATIONS em Tempo de Execução: Implementar o fluxo moderno de permissão no Android 13+ (API 33+) com rememberLauncherForActivityResult.
  4. Implementar Boas Práticas de Segurança em Notificações (OWASP): Proteger deep links utilizando PendingIntent.FLAG_IMMUTABLE contra ataques de sequestro e mutação de Intents.
  5. Construir Notificações com NotificationCompat.Builder: Configurar ícone, título, texto de alerta e intenção de clique para reabrir o app de forma limpa.

🏗️ Arquitetura e Grafo de Navegação com Notificações

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]

🛡️ Boas Práticas de Segurança em Notificações (OWASP Mobile)

[!IMPORTANT] Vulnerabilidade de Intent Mutation em Notificações: Versões antigas do Android permitiam a criação de PendingIntent mutá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:

  1. Uso de PendingIntent.FLAG_IMMUTABLE: Garante que a intenção disparada pela notificação não possa ser interceptada ou alterada por processos externos.
  2. Consentimento Explícito (POST_NOTIFICATIONS): Não disparamos notificações sem autorização prévia do usuário no Android 13+.
  3. Higiene de Versionamento: O arquivo .gitignore isola caches e dados temporários do projeto.

📖 Dicionário Técnico 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.

🛠️ 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: Portais Dimensionais (ou android_p14_portais_push)
    • Package name: br.com.curso.portais
    • 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 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.


📄 Passo 3: Declarando Permissões no Manifesto (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>

🎴 Passo 4: Modelo de Dados das Dimensões (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"

🔔 Passo 5: Criação do Canal de Notificação e Disparo Seguro

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)
}

🧭 Passo 6: Grafo de Navegação com Passagem de Argumentos

@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() }
            )
        }
    }
}

📋 Passo 7: Tela de Lista com Verificação de Permissão Android 13+

@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)
                    }
                }
            }
        }
    }
}

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

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).

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

  1. 🚀 Ações na Notificação (Notification Actions): Adicione um botão “FECHAR FENDA” diretamente dentro da notificação usando .addAction().
  2. 🖼️ Notificação com Estilo Expandido (BigTextStyle): Exiba a descrição completa da dimensão com NotificationCompat.BigTextStyle().
  3. 🔔 Som Personalizado de Portal: Associe um arquivo .mp3 de efeito sonoro de ficção científica da pasta res/raw ao canal de notificação.

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

Disponível em: app/src/main/java/br/com/curso/portais/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_p14_portais_push

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_p14_portais_push.