Tema 1D. Intents, permisos y entorno Android
- Bloque: B1 — Fundamentos: Kotlin, Compose y entorno Android
- Duración aproximada: 4 horas
- RA1 — Aplica tecnologías de desarrollo para dispositivos móviles evaluando sus características y capacidades.
| Código | Criterio |
|---|---|
| RA1-a | Se han analizado las limitaciones que plantea la ejecución de aplicaciones en los dispositivos móviles. |
| RA1-c | Se han instalado, configurado y utilizado entornos de trabajo para el desarrollo de aplicaciones para dispositivos móviles. |
| RA1-d | Se han identificado configuraciones que clasifican los dispositivos móviles en base a sus características. |
| RA1-e | Se han descrito perfiles que establecen la relación entre el dispositivo y la aplicación. |
| RA1-f | Se ha analizado la estructura de aplicaciones existentes para dispositivos móviles identificando las clases utilizadas. |
| RA1-g | Se han realizado modificaciones sobre aplicaciones existentes. |
| RA1-h | Se han utilizado emuladores para comprobar el funcionamiento de las aplicaciones. |
1. El ciclo de vida de una Activity#
Antes de estudiar los Intents, es esencial comprender el ciclo de vida de una Activity, porque determina cuándo se crean, pausan, reanudan y destruyen las pantallas de una app.
Activity creada
│
onCreate() ← inicialización: setContent, configuración
│
onStart() ← la Activity es visible
│
onResume() ← la Activity tiene el foco y el usuario interactúa
│
[EN USO]
│
onPause() ← otra Activity toma el foco (ej. diálogo, llamada)
│
onStop() ← la Activity ya no es visible (pasa a background)
│
onDestroy() ← Activity destruida (usuario sale o rotación de pantalla)
│
(fin)Cuándo usar cada callback#
| Callback | Usar para |
|---|---|
onCreate |
Inicialización única: setContent, cargar datos iniciales |
onStart / onStop |
Iniciar/detener animaciones, sensores de bajo consumo |
onResume / onPause |
Cámara, audio en primer plano, actualizar UI en tiempo real |
onDestroy |
Liberar recursos que no gestionan ciclo de vida automáticamente |
En Jetpack Compose, la MainActivity raramente necesita sobrescribir estos callbacks, porque Compose y los ViewModel gestionan el ciclo de vida automáticamente. Sin embargo, es importante conocerlos para entender qué ocurre cuando la app va a background o se rota la pantalla.
1class MainActivity : ComponentActivity() {
2 override fun onCreate(savedInstanceState: Bundle?) {
3 super.onCreate(savedInstanceState)
4 enableEdgeToEdge()
5 setContent {
6 AppFlixTheme {
7 AppFlixApp()
8 }
9 }
10 }
11
12 // En apps con Compose y ViewModel, raramente necesitas sobrescribir onPause/onStop
13 // Los recursos multimedia (cámara, audio) se gestionarán en el Bloque 4
14}2. Android Manifest#
El AndroidManifest.xml es el archivo de configuración central de la app. Define qué componentes tiene, qué permisos necesita y cómo debe comportarse el sistema.
1<!-- app/src/main/AndroidManifest.xml -->
2<?xml version="1.0" encoding="utf-8"?>
3<manifest xmlns:android="http://schemas.android.com/apk/res/android">
4
5 <!-- Permisos que la app necesita (se verá en la sección 4) -->
6 <uses-permission android:name="android.permission.INTERNET" />
7
8 <application
9 android:name=".AppFlixApplication"
10 android:icon="@mipmap/ic_launcher"
11 android:label="@string/app_name"
12 android:theme="@style/Theme.AppFlix"
13 android:supportsRtl="true">
14
15 <!-- Activity principal — punto de entrada -->
16 <activity
17 android:name=".MainActivity"
18 android:exported="true"
19 android:windowSoftInputMode="adjustResize">
20
21 <!-- Intent filter que la convierte en la activity de inicio -->
22 <intent-filter>
23 <action android:name="android.intent.action.MAIN" />
24 <category android:name="android.intent.category.LAUNCHER" />
25 </intent-filter>
26 </activity>
27
28 <!-- Otras Activities deben declararse aquí también -->
29 <!-- En apps con Compose y Navigation, normalmente solo hay una Activity -->
30
31 </application>
32</manifest>En arquitecturas modernas con Jetpack Compose y Navigation Compose, es habitual tener una sola Activity y gestionar toda la navegación mediante Composables. Esto simplifica mucho el Manifest. Verás este patrón en el Bloque 2.
3. Intents#
Un Intent es un mensaje que permite a los componentes de Android comunicarse entre sí: activar otra Activity, enviar datos entre pantallas o solicitar una acción al sistema o a otra app.
Hay dos tipos de Intents:
| Tipo | Descripción | Uso |
|---|---|---|
| Explícito | Especifica exactamente qué componente activar | Navegar a otra Activity de la misma app |
| Implícito | Declara una acción; el sistema elige el componente | Abrir una URL, compartir contenido, hacer una llamada |
3.1 Intent explícito#
1// En una app con múltiples Activities (patrón menos común con Compose)
2// Navegar de MainActivity a DetalleActivity pasando un ID
3
4// Desde la Activity de origen
5val intent = Intent(this, DetalleActivity::class.java).apply {
6 putExtra("PELICULA_ID", 42)
7 putExtra("PELICULA_TITULO", "Dune: Parte 2")
8}
9startActivity(intent)
10
11// En DetalleActivity — recuperar los datos
12class DetalleActivity : ComponentActivity() {
13 override fun onCreate(savedInstanceState: Bundle?) {
14 super.onCreate(savedInstanceState)
15 val id = intent.getIntExtra("PELICULA_ID", -1)
16 val titulo = intent.getStringExtra("PELICULA_TITULO") ?: ""
17
18 setContent {
19 AppFlixTheme {
20 PantallaDetalle(id = id, titulo = titulo)
21 }
22 }
23 }
24}Arquitectura moderna: En apps con Compose y Navigation Compose (Bloque 2), la navegación entre pantallas se gestiona con
NavController.navigate("detalle/$id"), sin necesidad de múltiples Activities. Los Intents explícitos se usan principalmente para integrar componentes externos (cámara del sistema, galería, etc.).
3.2 Intent implícito#
Los Intents implícitos permiten delegar acciones al sistema o a otras apps instaladas:
1@Composable
2fun AccionesCompartir(titulo: String, url: String) {
3 val context = LocalContext.current
4
5 Column(
6 modifier = Modifier.padding(16.dp),
7 verticalArrangement = Arrangement.spacedBy(8.dp)
8 ) {
9 // Compartir texto con cualquier app instalada
10 Button(onClick = {
11 val intent = Intent(Intent.ACTION_SEND).apply {
12 type = "text/plain"
13 putExtra(Intent.EXTRA_SUBJECT, "Te recomiendo esta película")
14 putExtra(Intent.EXTRA_TEXT, "Mira '$titulo': $url")
15 }
16 context.startActivity(
17 Intent.createChooser(intent, "Compartir con...")
18 )
19 }) { Text("Compartir") }
20
21 // Abrir URL en el navegador
22 Button(onClick = {
23 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
24 // Verificar que hay una app que puede manejar el Intent
25 if (intent.resolveActivity(context.packageManager) != null) {
26 context.startActivity(intent)
27 }
28 }) { Text("Ver en web") }
29
30 // Enviar email
31 Button(onClick = {
32 val intent = Intent(Intent.ACTION_SENDTO).apply {
33 data = Uri.parse("mailto:")
34 putExtra(Intent.EXTRA_EMAIL, arrayOf("soporte@appflix.com"))
35 putExtra(Intent.EXTRA_SUBJECT, "Consulta sobre $titulo")
36 }
37 if (intent.resolveActivity(context.packageManager) != null) {
38 context.startActivity(intent)
39 }
40 }) { Text("Contactar soporte") }
41
42 // Hacer una llamada
43 Button(onClick = {
44 val intent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:+34600000000"))
45 context.startActivity(intent)
46 }) { Text("Llamar al soporte") }
47 }
48}3.3 Recibir un resultado de otro componente#
Cuando necesitas un resultado de vuelta (ej. una foto tomada con la cámara del sistema, o un archivo seleccionado), usa ActivityResultLauncher:
1@Composable
2fun SeleccionarImagen() {
3 val context = LocalContext.current
4 var imagenUri by remember { mutableStateOf<Uri?>(null) }
5
6 // Registrar el launcher para obtener resultado
7 val seleccionadorImagen = rememberLauncherForActivityResult(
8 contract = ActivityResultContracts.GetContent()
9 ) { uri: Uri? ->
10 imagenUri = uri // uri es null si el usuario canceló
11 }
12
13 Column(
14 modifier = Modifier.padding(16.dp),
15 horizontalAlignment = Alignment.CenterHorizontally
16 ) {
17 Button(onClick = { seleccionadorImagen.launch("image/*") }) {
18 Text("Seleccionar imagen")
19 }
20
21 imagenUri?.let { uri ->
22 Spacer(modifier = Modifier.height(16.dp))
23 AsyncImage( // de la librería Coil, se añade en Bloque 3
24 model = uri,
25 contentDescription = "Imagen seleccionada",
26 modifier = Modifier.size(200.dp).clip(RoundedCornerShape(8.dp))
27 )
28 }
29 }
30}4. Permisos en Android 🔑#
Android protege el acceso a recursos sensibles (cámara, ubicación, contactos, micrófono) mediante un sistema de permisos. El usuario debe conceder estos permisos explícitamente.
4.1 Tipos de permisos#
| Tipo | Descripción | Ejemplo | ¿Requiere diálogo? |
|---|---|---|---|
| Normal | Bajo riesgo, se conceden automáticamente | INTERNET, VIBRATE |
No |
| Peligroso | Acceso a datos sensibles o hardware | CAMERA, READ_CONTACTS, LOCATION |
Sí, en tiempo de ejecución |
| Firma | Solo para apps del mismo desarrollador | READ_CONTACTS del sistema |
No |
Los permisos normales (como INTERNET) solo se declaran en el Manifest y se conceden automáticamente. Los permisos peligrosos requieren que el usuario los apruebe en tiempo de ejecución (desde Android 6.0, API 23).
4.2 Declarar permisos en el Manifest#
Todos los permisos deben declararse en el Manifest, independientemente de si son normales o peligrosos:
1<manifest>
2 <!-- Permiso normal (concedido automáticamente) -->
3 <uses-permission android:name="android.permission.INTERNET" />
4
5 <!-- Permisos peligrosos (requieren aprobación del usuario) -->
6 <uses-permission android:name="android.permission.CAMERA" />
7 <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- Android 13+ -->
8 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
9 android:maxSdkVersion="32" /> <!-- solo en Android ≤ 12 -->
10
11 <!-- Características hardware opcionales -->
12 <uses-feature android:name="android.hardware.camera" android:required="false" />
13</manifest>4.3 Solicitar permisos en tiempo de ejecución (Compose)#
1@Composable
2fun PantallaConPermisoCamara() {
3 val context = LocalContext.current
4 var tienePermiso by remember {
5 mutableStateOf(
6 ContextCompat.checkSelfPermission(
7 context,
8 Manifest.permission.CAMERA
9 ) == PackageManager.PERMISSION_GRANTED
10 )
11 }
12 var permisoDenegadoPermanentemente by remember { mutableStateOf(false) }
13
14 // Launcher para solicitar un único permiso
15 val solicitarPermiso = rememberLauncherForActivityResult(
16 contract = ActivityResultContracts.RequestPermission()
17 ) { concedido ->
18 tienePermiso = concedido
19 if (!concedido) {
20 // Comprobar si el usuario marcó "No volver a preguntar"
21 // (solo posible desde una Activity, no directamente desde Compose)
22 }
23 }
24
25 Column(
26 modifier = Modifier.fillMaxSize().padding(16.dp),
27 horizontalAlignment = Alignment.CenterHorizontally,
28 verticalArrangement = Arrangement.Center
29 ) {
30 when {
31 tienePermiso -> {
32 Icon(
33 Icons.Default.CameraAlt,
34 contentDescription = null,
35 modifier = Modifier.size(64.dp),
36 tint = MaterialTheme.colorScheme.primary
37 )
38 Spacer(modifier = Modifier.height(16.dp))
39 Text("Permiso de cámara concedido ✓")
40 Text("Aquí iría la vista de cámara (Bloque 4)")
41 }
42
43 permisoDenegadoPermanentemente -> {
44 Icon(
45 Icons.Default.Block,
46 contentDescription = null,
47 modifier = Modifier.size(64.dp),
48 tint = MaterialTheme.colorScheme.error
49 )
50 Text("Permiso de cámara denegado permanentemente")
51 Spacer(modifier = Modifier.height(8.dp))
52 OutlinedButton(
53 onClick = {
54 // Abrir Ajustes del sistema para que el usuario lo conceda manualmente
55 val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
56 data = Uri.fromParts("package", context.packageName, null)
57 }
58 context.startActivity(intent)
59 }
60 ) { Text("Abrir Ajustes") }
61 }
62
63 else -> {
64 Text(
65 "Esta función necesita acceso a la cámara para fotografiar portadas.",
66 style = MaterialTheme.typography.bodyMedium
67 )
68 Spacer(modifier = Modifier.height(16.dp))
69 Button(onClick = { solicitarPermiso.launch(Manifest.permission.CAMERA) }) {
70 Text("Conceder permiso de cámara")
71 }
72 }
73 }
74 }
75}4.4 Solicitar múltiples permisos#
1@Composable
2fun SolicitarPermisosMultimedia() {
3 val context = LocalContext.current
4
5 // En Android 13+, los permisos de imágenes, vídeo y audio son separados
6 val permisos = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
7 arrayOf(
8 Manifest.permission.READ_MEDIA_IMAGES,
9 Manifest.permission.READ_MEDIA_VIDEO
10 )
11 } else {
12 arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
13 }
14
15 var permisosEstado by remember {
16 mutableStateOf(
17 permisos.associateWith { permiso ->
18 ContextCompat.checkSelfPermission(context, permiso) ==
19 PackageManager.PERMISSION_GRANTED
20 }
21 )
22 }
23
24 // Launcher para múltiples permisos a la vez
25 val solicitarPermisos = rememberLauncherForActivityResult(
26 contract = ActivityResultContracts.RequestMultiplePermissions()
27 ) { resultados ->
28 permisosEstado = resultados
29 }
30
31 val todosConcentidos = permisosEstado.all { it.value }
32
33 Column(modifier = Modifier.padding(16.dp)) {
34 if (todosConcentidos) {
35 Text("✓ Todos los permisos concedidos")
36 } else {
37 Text("Se necesitan permisos para acceder a la galería")
38 Spacer(modifier = Modifier.height(8.dp))
39 Button(onClick = { solicitarPermisos.launch(permisos) }) {
40 Text("Solicitar permisos")
41 }
42 }
43 }
44}5. Configuración del entorno de desarrollo#
5.1 Android Studio — aspectos clave#
Android Studio es el IDE oficial para el desarrollo Android, basado en IntelliJ IDEA. Los aspectos más importantes para el trabajo diario son:
SDK Manager (Tools → SDK Manager):
- Gestiona las versiones del SDK de Android instaladas.
- Para este curso: instala Android 14 (API 34) y Android 15 (API 35) como mínimo.
- SDK Tools: asegúrate de tener instalados
Android EmulatoryAndroid SDK Platform-Tools.
Device Manager (Tools → Device Manager):
- Crea y gestiona emuladores (AVD — Android Virtual Device).
- Recomendado para este curso: Pixel 6 con API 34 (x86_64).
Logcat (View → Tool Windows → Logcat):
- Muestra los logs de la app en tiempo real.
- Filtra por tag, nivel (Verbose/Debug/Info/Warning/Error) o texto.
- Imprescindible para depuración.
5.2 Logging con Logcat#
1import android.util.Log
2
3// Niveles de log (de menor a mayor severidad)
4Log.v("AppFlix", "Verbose: detalle exhaustivo") // Verbose
5Log.d("AppFlix", "Debug: información de depuración") // Debug
6Log.i("AppFlix", "Info: evento importante") // Info
7Log.w("AppFlix", "Warning: situación anómala") // Warning
8Log.e("AppFlix", "Error: fallo grave", excepcion) // Error
9
10// Ejemplo práctico
11class PeliculasRepository {
12 companion object {
13 private const val TAG = "PeliculasRepo"
14 }
15
16 suspend fun cargarPeliculas(): List<String> {
17 Log.d(TAG, "Iniciando carga de películas")
18 return try {
19 val resultado = listOf("Dune", "Inception") // simulado
20 Log.i(TAG, "Cargadas ${resultado.size} películas correctamente")
21 resultado
22 } catch (e: Exception) {
23 Log.e(TAG, "Error al cargar películas", e)
24 emptyList()
25 }
26 }
27}5.3 Emuladores y dispositivos reales#
Emulador:
- Ventajas: sin necesidad de dispositivo físico, fácil de configurar, permite simular condiciones (sin red, batería baja, GPS específico).
- Inconvenientes: más lento que un dispositivo real, algunas APIs de hardware no están disponibles (NFC, Bluetooth real).
Dispositivo real:
- Activar Opciones de desarrollador: Ajustes → Acerca del teléfono → pulsa 7 veces en “Número de compilación”.
- Activar Depuración USB: Opciones de desarrollador → Depuración USB.
- Conectar por USB o mediante ADB WiFi (Android 11+: Opciones de desarrollador → Depuración inalámbrica).
5.4 Estructura de un proyecto Android#
Vista general de la estructura típica de un proyecto Android con Jetpack Compose, tipo de estructura Android:
MiApp/
├── app/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/com.ejemplo.miapp/
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── ui/ ← pantallas y componentes
│ │ │ │ │ ├── theme/ ← colores, tipografía, tema
│ │ │ │ │ └── components/ ← composables reutilizables
│ │ │ │ └── data/ ← modelos y repositorios (Bloques 2-3)
│ │ │ ├── res/
│ │ │ │ ├── drawable/ ← imágenes vectoriales (SVG → XML)
│ │ │ │ ├── mipmap/ ← iconos de la app en distintas densidades
│ │ │ │ ├── values/
│ │ │ │ │ ├── strings.xml ← textos de la app (internacionalización)
│ │ │ │ │ ├── colors.xml ← colores base del tema
│ │ │ │ │ └── themes.xml ← tema Material
│ │ │ │ └── xml/ ← configuraciones XML (backup, network, etc.)
│ │ │ └── AndroidManifest.xml
│ │ ├── test/ ← tests unitarios (JUnit)
│ │ └── androidTest/ ← tests de instrumentación (Compose UI Test)
│ ├── build.gradle.kts ← dependencias y configuración del módulo app
│ └── proguard-rules.pro ← reglas de ofuscación (release)
├── gradle/
│ └── libs.versions.toml ← versiones centralizadas (version catalog)
├── build.gradle.kts ← configuración del proyecto
└── settings.gradle.kts ← módulos del proyecto5.5 Version Catalog (gestión moderna de dependencias)#
Android Studio 2024+ usa un catálogo de versiones centralizado para gestionar las dependencias:
1# gradle/libs.versions.toml
2[versions]
3agp = "8.7.2"
4kotlin = "2.0.21"
5composeBom = "2024.10.00"
6activityCompose = "1.9.3"
7lifecycleViewmodelCompose = "2.8.7"
8
9[libraries]
10androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
11androidx-ui = { group = "androidx.compose.ui", name = "ui" }
12androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
13androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
14
15[plugins]
16android-application = { id = "com.android.application", version.ref = "agp" }
17kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
18kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }1// build.gradle.kts (app) — referencia al catálogo con libs.*
2dependencies {
3 val bom = platform(libs.androidx.compose.bom)
4 implementation(bom)
5 implementation(libs.androidx.ui)
6 implementation(libs.androidx.material3)
7 implementation(libs.androidx.activity.compose)
8}Ejemplo práctico completo: gestión de permisos en AppFlix#
Este ejemplo muestra un patrón completo y reutilizable para gestionar permisos en Compose:
1package com.ejemplo.appflix.ui.permisos
2
3import android.Manifest
4import android.content.Intent
5import android.net.Uri
6import android.os.Build
7import android.provider.Settings
8import androidx.compose.foundation.layout.*
9import androidx.compose.material.icons.Icons
10import androidx.compose.material.icons.filled.*
11import androidx.compose.material3.*
12import androidx.compose.runtime.*
13import androidx.compose.ui.Alignment
14import androidx.compose.ui.Modifier
15import androidx.compose.ui.platform.LocalContext
16import androidx.compose.ui.tooling.preview.Preview
17import androidx.compose.ui.unit.dp
18import androidx.core.content.ContextCompat
19import androidx.activity.compose.rememberLauncherForActivityResult
20import androidx.activity.result.contract.ActivityResultContracts
21
22// ─── PantallaGestionPermisos.kt ──────────────────────────────────────────────────────────────────
23
24// Estado del permiso — representado como sealed class
25sealed class EstadoPermiso {
26 data object Concedido : EstadoPermiso()
27 data object Pendiente : EstadoPermiso()
28 data object Denegado : EstadoPermiso()
29 data object DenegadoPermanentemente : EstadoPermiso()
30}
31
32@Composable
33fun PantallaGestionPermisos() {
34 val context = LocalContext.current
35
36 // Comprobación inicial del estado del permiso de cámara
37 var estadoPermiso by remember {
38 val concedido = ContextCompat.checkSelfPermission(
39 context, Manifest.permission.CAMERA
40 ) == android.content.pm.PackageManager.PERMISSION_GRANTED
41
42 mutableStateOf(
43 if (concedido) EstadoPermiso.Concedido else EstadoPermiso.Pendiente
44 )
45 }
46
47 val solicitarPermiso = rememberLauncherForActivityResult(
48 ActivityResultContracts.RequestPermission()
49 ) { concedido ->
50 estadoPermiso = if (concedido) EstadoPermiso.Concedido
51 else EstadoPermiso.Denegado
52 }
53
54 Scaffold(
55 topBar = { TopAppBar(title = { Text("Permisos de AppFlix") }) }
56 ) { padding ->
57 Column(
58 modifier = Modifier
59 .fillMaxSize()
60 .padding(padding)
61 .padding(24.dp),
62 horizontalAlignment = Alignment.CenterHorizontally,
63 verticalArrangement = Arrangement.Center
64 ) {
65 // Icono según estado
66 val (icono, colorIcono, descripcion) = when (estadoPermiso) {
67 EstadoPermiso.Concedido -> Triple(
68 Icons.Default.CheckCircle,
69 MaterialTheme.colorScheme.primary,
70 "Cámara disponible"
71 )
72 EstadoPermiso.Pendiente -> Triple(
73 Icons.Default.CameraAlt,
74 MaterialTheme.colorScheme.onSurfaceVariant,
75 "La app necesita acceso a la cámara para fotografiar portadas"
76 )
77 EstadoPermiso.Denegado,
78 EstadoPermiso.DenegadoPermanentemente -> Triple(
79 Icons.Default.Block,
80 MaterialTheme.colorScheme.error,
81 "Sin acceso a la cámara"
82 )
83 }
84
85 Icon(icono, contentDescription = null, modifier = Modifier.size(72.dp), tint = colorIcono)
86 Spacer(modifier = Modifier.height(16.dp))
87 Text(descripcion, style = MaterialTheme.typography.bodyLarge)
88 Spacer(modifier = Modifier.height(32.dp))
89
90 // Acción según estado
91 when (estadoPermiso) {
92 EstadoPermiso.Concedido -> {
93 Text("✓ Puedes usar la cámara en AppFlix")
94 }
95 EstadoPermiso.Pendiente -> {
96 Button(
97 onClick = { solicitarPermiso.launch(Manifest.permission.CAMERA) },
98 modifier = Modifier.fillMaxWidth()
99 ) { Text("Conceder permiso de cámara") }
100 }
101 EstadoPermiso.Denegado,
102 EstadoPermiso.DenegadoPermanentemente -> {
103 OutlinedButton(
104 onClick = {
105 val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
106 data = Uri.fromParts("package", context.packageName, null)
107 }
108 context.startActivity(intent)
109 },
110 modifier = Modifier.fillMaxWidth()
111 ) { Text("Abrir Ajustes del sistema") }
112 }
113 }
114 }
115 }
116}
117
118@Preview(showBackground = true)
119@Composable
120fun PantallaPermsisosPreview() {
121 MaterialTheme {
122 PantallaGestionPermisos()
123 }
124}Actividad de consolidación del Bloque 1#
Llegados a este punto, has completado los fundamentos del Bloque 1. El proyecto vertebrador AppFlix debe tener en este momento:
- Pantalla de bienvenida con campo de nombre y botón habilitado condicionalmente (B1-T1 B).
- Pantalla de listado con búsqueda, filtros por género y toggle de favorito (B1-T1 C).
- Gestión básica de al menos un permiso (B1-T1 D).
Tarea de consolidación (para entregar):
Añade a la app del listado un botón “Compartir lista” que use un Intent implícito para compartir los títulos de las películas filtradas actualmente. El mensaje compartido debe incluir el número de resultados y los títulos, uno por línea.
Utiliza
Intent.ACTION_SENDcontype = "text/plain"y construye el texto con la lista filtrada.*