ISW-126 · Capacidades y permisos · Desarrollo de Aplicaciones Móviles I

El dispositivo y sus llaves: cámara, GPS y permisos

Esto es lo más distintivamente móvil que existe: una app que usa la cámara, sabe dónde está y pide permiso para hacerlo. Es exactamente lo que un formulario web no puede hacer.

Pagamos una deuda: en la clase 1 nombramos “el sistema operativo como guardián” como uno de los cuatro pilares de lo móvil, y no lo desarrollamos. Este es ese pilar.

1 Por qué esto es lo más “móvil” de todo

Un teléfono no es solo una pantalla: es una caja de sensores —cámara, GPS, micrófono, acelerómetro, red— que la app puede usar. Pero el usuario es el dueño de esos sensores, y el sistema operativo actúa como guardián: nada se usa sin permiso.

Dos ideas que gobiernan todo el tema: primero, las capacidades se piden con una API multiplataforma llamada MAUI Essentials, así el mismo código funciona en Android y iOS. Segundo, cada acceso sensible pasa por el modelo de permisos en tiempo de ejecución: la app pregunta, el usuario decide, y tu código debe manejar el “no” con la misma seriedad que el “sí”.

2 MAUI Essentials: el catálogo

Una sola API para las capacidades del dispositivo. Estas son las que más se usan; la app de referencia utiliza las tres primeras.

CapacidadAPI¿Necesita permiso?
Cámara / fotoMediaPicker.DefaultSí — cámara
UbicaciónGeolocation.DefaultSí — ubicación
ConectividadConnectivity.CurrentNo
Almacenamiento privadoFileSystem, PreferencesNo
Datos sensibles cifradosSecureStorageNo
SensoresAccelerometer, Compass…No (algunos sí)
Regla mental: si el dato es del usuario o de su entorno (su cara, su ubicación, sus contactos), hay permiso de por medio. Si es de la app (sus propios archivos, sus preferencias), no.

3 El modelo de permisos en tiempo de ejecución

El corazón del tema. No basta con “usar la cámara”: primero se comprueba el estado, luego se pide, y —lo que casi nadie maneja bien— hay que contemplar que el usuario lo niegue, incluso para siempre. Juega con el simulador: intenta tomar la foto y niega el permiso un par de veces.

CheckStatusAsync→ RequestAsync→ Granted ✓/ Denied→ Denied para siempre → Ajustes
NotasApp · nueva nota
Permiso: sin determinar
Sin foto
📷

¿Permitir que NotasApp use la cámara?

Registro de las llamadas a la API

Toca “Tomar foto” para empezar…

Este es exactamente el patrón que la app de referencia encapsula en CapacidadesService, y sirve para cualquier permiso:

private async Task<PermissionStatus> PedirPermisoAsync<T>()
    where T : Permissions.BasePermission, new()
{
    // a) ¿Ya lo tenemos? Nunca pidas de más.
    var estado = await Permissions.CheckStatusAsync<T>();
    if (estado == PermissionStatus.Granted) return estado;

    // b) Pedirlo: el sistema muestra el diálogo nativo.
    estado = await Permissions.RequestAsync<T>();

    // c) Denegado y sin poder volver a preguntar = denegado PARA SIEMPRE.
    //    La única salida es que el usuario lo active en Ajustes.
    if (estado != PermissionStatus.Granted && !Permissions.ShouldShowRationale<T>())
        AppInfo.Current.ShowSettingsUI();

    return estado;
}
El caso que separa a un junior de un senior: manejar el “denegado para siempre”. Si sigues llamando a RequestAsync, el sistema ya no muestra nada y tu app parece rota. Hay que detectar ese estado y mandar al usuario a Ajustes con AppInfo.Current.ShowSettingsUI().

4 Cámara y ubicación

Con el permiso resuelto, usar la capacidad es sorprendentemente corto. Todo es asíncrono: nunca bloqueas la UI esperando al hardware.

Cámara (MediaPicker)

if (!MediaPicker.Default.IsCaptureSupported) return null;

var foto = await MediaPicker.Default.CapturePhotoAsync();  // abre la cámara
if (foto is null) return null;                             // el usuario canceló

// Copiar la foto al almacenamiento privado de la app para conservarla:
var destino = Path.Combine(FileSystem.AppDataDirectory, foto.FileName);
using var origen = await foto.OpenReadAsync();
using var salida = File.OpenWrite(destino);
await origen.CopyToAsync(salida);

Ubicación (Geolocation)

var request = new GeolocationRequest(GeolocationAccuracy.Medium,
                                     TimeSpan.FromSeconds(10));
Location? loc = await Geolocation.Default.GetLocationAsync(request);
// loc.Latitude, loc.Longitude
Elige la precisión mínima que te sirva. Medium ubica “dónde escribí la nota” sin encender el GPS de alta precisión, que castiga la batería —una decisión de recursos, como vimos en la clase 1.

5 Conectividad: el offline-first en acción

No necesita permiso, pero es la capacidad que más cambia la arquitectura. La app debe saber si hay red y comportarse bien cuando no la hay. Prueba el interruptor.

Con conexión
// Estado actual:
bool hayInternet = Connectivity.Current.NetworkAccess == NetworkAccess.Internet;

// Reaccionar a los cambios en vivo:
Connectivity.Current.ConnectivityChanged += (s, e) =>
{
    bool online = e.NetworkAccess == NetworkAccess.Internet;
    // mostrar/ocultar el banner, reanudar la sincronización pendiente...
};

En la app de referencia, NotasViewModel lee HayInternet al cargar y enciende un banner “sin conexión”. Como todo se guarda en SQLite local, la app sigue siendo usable sin red: ese es el corazón del offline-first.

6 Declaraciones de plataforma: el paso que todos olvidan

Pedir el permiso en código no basta. Cada sistema exige declarar por adelantado qué capacidades usará la app. Si faltan estas líneas, la app compila pero el permiso falla en silencio —el bug más frustrante para un principiante.

Android — Platforms/Android/AndroidManifest.xml

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

iOS — Platforms/iOS/Info.plist

<key>NSCameraUsageDescription</key>
<string>Para adjuntar una foto a tus notas.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Para guardar dónde escribiste la nota.</string>
En iOS el texto es obligatorio y visible: ese <string> es justo lo que el usuario lee en el diálogo. Un texto vago (“la app necesita ubicación”) baja la tasa de aceptación; uno claro (“para guardar dónde escribiste la nota”) la sube.

7 Cierre

Lo que te llevas hoy

  • MAUI Essentials da una API única para las capacidades del dispositivo en Android e iOS.
  • El permiso sigue un flujo: CheckStatusAsync → RequestAsync → manejar granted / denied / denegado para siempre.
  • El “denegado para siempre” se resuelve mandando al usuario a Ajustes con ShowSettingsUI().
  • La conectividad no pide permiso, pero define la arquitectura: offline-first, la app sigue usable sin red.
  • Sin declarar los permisos en el manifiesto (Android) y el Info.plist (iOS), nada funciona.

Todo lo de este documento está implementado y comentado en la app de referencia NotasApp: mira Services/CapacidadesService.cs para el patrón de permisos, cámara y GPS, y ViewModels/DetalleViewModel.cs para verlo usado desde la pantalla. La guía completa mapea dónde vive cada pieza.