SDK de Android: Domar el administrador de batería
Android es famoso por eliminar agresivamente los servicios en segundo plano. Los fabricantes de equipos originales chinos (como Xiaomi, Huawei) incluso tienen administradores de energía personalizados que rompen por completo los límites estándar de ejecución en segundo plano de Android.
Esta página explica por qué Android elimina su aplicación y cómo las configuraciones específicas de Android de Tracelet lo resuelven en el mundo real.
Dependencia opcional: ubicación GMS de alta precisión
El SDK de Android de Tracelet puede utilizar Ubicación de servicios de Google Play — FusedLocationProviderClient
(mejor precisión y batería), hardware reconocimiento de actividad (caminar / conducir / quieto),
y hardware geocercas. Para mantener el SDK liviano y utilizable en dispositivos sin Google, este
la dependencia no está incluida. Lo agrega solo si desea la ruta de alta precisión:
// android/app/build.gradle.kts
dependencies {
implementation("com.google.android.gms:play-services-location:21.3.0")
}Es opcional y el SDK se degrada suavemente. Sin play-services-location, Tracelet
recurre al estándar AOSP LocationManager (GPS simple). El seguimiento todavía funciona; solo
perderá los beneficios de precisión/batería del proveedor fusionado, el reconocimiento de actividad del hardware y el hardware
geocercado. Recomendado: agréguelo a menos que esté dirigido específicamente a dispositivos sin Google/AOSP
(por ejemplo, Huawei sin GMS o ROM sin Google).
Versión mínima: 21.2.0. El código de Android de Tracelet llama a la interfaz basada en
API FusedLocationProviderClient y ActivityRecognitionClient, que solo se convirtieron
interfaces en play-services-location 21.2.0. Las versiones anteriores (por ejemplo, 19.0.0) incluyen estos
como clases concretas, por lo que llamar a ellas arroja
java.lang.IncompatibleClassChangeError en tiempo de ejecución. Tracelet publica un Gradle
restricción de dependencia que eleva play-services-location a 21.2.0+ automáticamente si
otra dependencia extrae una versión anterior, pero si fijas la versión tú mismo, mantenla en
21.2.0 o más reciente (recomendamos 21.3.0).
Con play-services-location | Sin él (respaldo de AOSP) |
|---|---|
| Ubicación fusionada (mejor precisión + batería) | GPS / red sin procesar a través de LocationManager |
Reconocimiento de actividad de hardware (onActivityChange) | Detección de movimiento solo con acelerómetro |
| Geocercas de hardware | Evaluación de geocercas de software (en SDK) |
Dependencia opcional: Play Integrity (certificación de dispositivo)
Si utiliza la función certificación de dispositivo de Tracelet (AttestationConfig), Android
El lado genera su token criptográfico a través de Google Play Integrity. al igual que
play-services-location, esta dependencia no está incluida; la agregas solo cuando
habilitar la atestación:
// android/app/build.gradle.kts
dependencies {
implementation("com.google.android.play:integrity:1.6.0")
}Es opcional y la certificación se degrada gradualmente. Sin Play Integrity
dependencia, todo lo demás en Tracelet funciona normalmente; solo la atestación se ve afectada:
AttestationConfig(enabled: true) registra una advertencia y Tracelet.getAttestationToken()
devuelve null en lugar de un token, en lugar de fallar. Agregue la dependencia solo si
habilite la certificación y necesite el veredicto de integridad de Play en Android. Ver el
Guía de certificación de dispositivos
para el flujo de trabajo completo y la verificación del lado del servidor.
Debe ser implementation, no compileOnly. Play Integrity es compileOnly en el interior
el SDK de Tracelet (por lo que nunca se envía ni se fuerza en aplicaciones que no utilizan la atestación). Si
habilita la atestación sin agregar la dependencia a su módulo aplicación como
implementation, las clases están ausentes en tiempo de ejecución y la atestación devuelve null de forma silenciosa.
Permisos y configuración de manifiesto
Tracelet inyecta automáticamente los permisos necesarios en su AndroidManifest.xml cuando compila su aplicación. Por defecto solicita:
ACCESS_COARSE_LOCATION&ACCESS_FINE_LOCATION(Requerido para seguimiento)ACCESS_BACKGROUND_LOCATION(Requerido para el seguimiento cuando la aplicación está cerrada)FOREGROUND_SERVICE&FOREGROUND_SERVICE_LOCATION(Requerido para ejecución continua en segundo plano)ACTIVITY_RECOGNITION(Requerido para el motor de detección de movimiento inteligente)POST_NOTIFICATIONS(Requerido para la interfaz de usuario del servicio de primer plano de Android 13+)SCHEDULE_EXACT_ALARM(Requerido para un seguimiento preciso del modo periódico)
Eliminación de permisos no utilizados (fusión de manifiesto)
Si su aplicación no necesita una característica específica (por ejemplo, su cliente no necesita detección de movimiento o no usa el modo periódico), puede eliminar el permiso por la fuerza usando la directiva tools:node="remove" de fusión de manifiesto de Android en su nivel de aplicación AndroidManifest.xml (android/app/src/main/AndroidManifest.xml).
El código nativo de Tracelet está completamente protegido con checkSelfPermission(). La falta de permisos provocará retrocesos elegantes, no bloqueos.
| Permiso | Efecto al eliminarlo (tools:node="remove") |
|---|---|
ACTIVITY_RECOGNITION | Seguro. Vuelve a la detección de movimiento básica solo con acelerómetro. La transmisión onActivityChange no se activa (sin clasificación para caminar/correr/conducir). |
ACCESS_BACKGROUND_LOCATION | Seguro. Android 10+ restringirá el seguimiento cuando la aplicación esté completamente en segundo plano o se haya eliminado. |
SCHEDULE_EXACT_ALARM | Seguro. El modo periódico recurre al uso de temporizadores WorkManager que funcionan con batería, pero son inexactos. |
POST_NOTIFICATIONS | Seguro. La notificación persistente del servicio en primer plano está oculta en Android 13+. El servicio aún funciona. |
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS | Seguro. No podrás solicitar exenciones de optimización de batería del sistema operativo. |
FOREGROUND_SERVICE / FOREGROUND_SERVICE_LOCATION | Seguro para aplicaciones de solo geocerca/solo periódicas. Quitarlas desactiva el seguimiento continuo en primer plano (start()), pero la geocerca estándar y el modo periódico siguen funcionando. Obligatorio para el seguimiento continuo en segundo plano. |
ACCESS_FINE_LOCATION / COARSE | INSEGURO. Sin ningún permiso de ubicación, Tracelet no puede obtener posiciones. |
Política de servicio en primer plano de Google Play (vigente a partir del 28 de octubre de 2026). La geocerca es
ya no es un caso de uso permitido para la ubicación del servicio en primer plano. Si su aplicación utiliza un
servicio en primer plano únicamente para geocercas, debes eliminar
FOREGROUND_SERVICE_LOCATION (y FOREGROUND_SERVICE) de su fusionado
manifiesto. El modo de geofence estándar de Tracelet utiliza la API Geofence nativa y no
no iniciar un servicio en primer plano, por lo que las aplicaciones que solo utilizan geocercas son compatibles, solo
elimine los permisos siguientes y mantenga foregroundService.enabled = false. Ver el
Guía de geovallas
para más detalles.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Geofence-only / periodic-only app: no continuous foreground tracking -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" tools:node="remove" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" tools:node="remove" />
</manifest>Ejemplo: eliminación de permisos de movimiento y fondo
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Client doesn't want Activity/Motion tracking -->
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" tools:node="remove" />
<!-- Client only wants foreground tracking -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" tools:node="remove" />
<!-- Remove Exact Alarms -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" tools:node="remove" />
</manifest>Reglas automáticas de ProGuard / R8
Tracelet utiliza en gran medida servicios Kotlin en segundo plano, enlaces JNA para el núcleo de Rust y canales reflectantes de Pigeon. Si se ofuscan o se reducen incorrectamente, el seguimiento en segundo plano fallará silenciosamente cuando la aplicación se compile para su lanzamiento.
No es necesario configurar manualmente ProGuard para Tracelet.
El complemento tracelet_android se envía automáticamente con consumer-rules.pro integrado. Cuando creas tu aplicación Flutter en modo de lanzamiento, el reductor R8 de Android extrae automáticamente estas reglas y garantiza que todas las clases necesarias (como los enlaces HeadlessTaskService, BootReceiver y Rust uniffi) sobrevivan a la reducción.
Escenario 1: El repartidor (seguimiento continuo)
Conceptos explorados: Modo Doze, Servicios en primer plano, Wakelocks
El problema
Su usuario es un repartidor de pizzas. Se metieron el teléfono en el bolsillo y apagaron la pantalla. Después de 15 minutos, Android ingresa al Modo Doze. Cierra el acceso a la red, pospone trabajos en segundo plano y limita severamente las reactivaciones de la CPU para ahorrar batería. Si su aplicación se basa en un temporizador simple para obtener GPS, Android simplemente se negará a ejecutar el temporizador.
Cómo lo resuelve Tracelet: servicio en primer plano
Un servicio en primer plano le dice a Android: “Oye, estoy haciendo algo increíblemente importante en este momento y el usuario es plenamente consciente de ello. No me mates”.
android: tl.AndroidConfig(
foregroundService: tl.ForegroundServiceConfig(
enabled: true,
channelName: 'Delivery Tracking',
notificationText: 'Tracking your route to the customer',
notificationOngoing: true, // User cannot swipe it away
showNotificationOnPauseOnly: false, // Always visible — see the section below
actions: ['Pause', 'Complete'], // Adds interactive buttons to the notification
),
)Al mostrar una notificación persistente en la barra de estado, Tracelet eleva la prioridad de su aplicación casi al nivel de la interfaz de usuario de primer plano. Android permitirá que Tracelet se ejecute indefinidamente, evitando las restricciones del modo Doze.
Personalizar la apariencia de las notificaciones
Para que la notificación persistente coincida con la marca de su aplicación, puede personalizar los íconos y colores:
android: tl.AndroidConfig(
foregroundService: tl.ForegroundServiceConfig(
enabled: true,
notificationTitle: 'Delivery Mode',
notificationText: 'Tracking your route...',
notificationColor: '#0F9D58', // Your brand hex color
notificationSmallIcon: 'ic_tracelet_icon', // The icon name
notificationLargeIcon: 'ic_large_logo',
),
)Reglas de iconos importantes:
- Ubicación del archivo: El archivo de imagen debe colocarse en la carpeta dibujable de su proyecto de Android (
android/app/src/main/res/drawable/ic_tracelet_icon.png). - Restricciones de íconos pequeños: Android requiere que
notificationSmallIconsea completamente plano, transparente y solo blanco. Si utiliza un logotipo de color, Android simplemente lo representará como un cuadrado gris o blanco sólido. - Propiedad de color: La propiedad
notificationColorteñirá el fondo de su pequeño ícono para que coincida con su marca.
Nota de Android 13+: Debe solicitar el permiso POST_NOTIFICATIONS antes de iniciar el servicio; de lo contrario, el sistema operativo suprimirá silenciosamente la notificación. Consulte la página Flutter SDK: Permisos para saber exactamente cómo solicitar esto desde su código Dart.
Actualizar la notificación durante el seguimiento (updateNotification())
Es necesario cambiar la notificación después de que el seguimiento ya haya comenzado, p. ¿Actualizar el título de “En ruta” a “Llegando”, intercambiar el texto o cambiar los botones de acción a mitad del viaje? Llamar a setConfig() con un nuevo ForegroundServiceConfig mantiene los valores, pero un cambio de solo notificación no vuelve a publicar la notificación que Android ya está mostrando; el nuevo contenido solo aparecería después de un reinicio del servicio no relacionado o una transición en primer plano.
Desde v3.6.8, Tracelet.updateNotification() actualiza la notificación en vivo, sin reiniciar el proceso de seguimiento:
// 1. Apply the new notification content.
await Tracelet.setConfig(
const tl.Config(
android: tl.AndroidConfig(
foregroundService: tl.ForegroundServiceConfig(
notificationTitle: 'Arriving',
notificationText: 'Your driver is 2 minutes away',
),
),
),
);
// 2. Repost the active notification with the new content.
await Tracelet.updateNotification();Seguro por diseño. updateNotification() nunca reinicia el seguimiento y no funciona cuando el servicio en primer plano no se está ejecutando actualmente (no hay nada que actualizar). En iOS, actualiza la Actividad en vivo en ejecución (cuando optó por una a través de liveActivityConfig); en la web no es posible, por lo que la misma llamada es segura en todas las plataformas.
Actualización silenciosa de la notificación (notificationOnlyAlertOnce)
Hay un problema en el patrón anterior. Android trata cada reenvío como una alerta nueva a menos que indiques lo contrario, por lo que una notificación que actualizas una vez por minuto reproduce el sonido de notificación una vez por minuto, mientras se ejecuta el seguimiento.
El canal de Tracelet se crea con la vibración y las luces desactivadas, por lo que es un sonido, no un zumbido, pero en el modo predeterminado notificationPriority es audible y los usuarios lo notan rápidamente.
Configure notificationOnlyAlertOnce para que solo las primeras alertas de publicación y todas las actualizaciones posteriores lleguen de forma silenciosa:
await Tracelet.setConfig(
const tl.Config(
android: tl.AndroidConfig(
foregroundService: tl.ForegroundServiceConfig(
notificationOnlyAlertOnce: true,
),
),
),
);Vale la pena configurar esto para cualquier notificación cuyo contenido cambie mientras se ejecuta el seguimiento (un recuento de paradas restantes, cargas pendientes, la geocerca actual, precisión en vivo) no solo para el temporizador a continuación.
¿Por qué no simplemente reducir la prioridad? Al colocar notificationPriority en low o min también se silencia el canal, pero también se silencia la primera publicación, y Android corrige la importancia de un canal cuando se crea. Una aplicación que ya está instalada con un canal de importancia predeterminado no puede reducirlo más tarde sin cambiar también channelId, lo que deja huérfano el canal anterior en la configuración del sistema. notificationOnlyAlertOnce no tiene ningún problema y funciona con cualquier prioridad.
El valor predeterminado es false, que preserva el comportamiento de todas las aplicaciones existentes. Nada cambia hasta que usted opte por suscribirse.
Ocultar la notificación mientras la aplicación está abierta (showNotificationOnPauseOnly)
showNotificationOnPauseOnly: true elimina la notificación de seguimiento mientras el usuario está mirando su aplicación y la devuelve cuando se va. Es una experiencia realmente mejor: la bandeja de notificaciones permanece limpia mientras la aplicación está en la pantalla, pero viene con una restricción estricta, y la restricción no es obvia.
Ocultar la notificación significa degradar el servicio. Android no tiene el concepto de servicio en primer plano sin una notificación, por lo que la única forma de ocultarlo es stopForeground(). El seguimiento continúa y el servicio continúa ejecutándose, pero mientras la notificación esté oculta, su proceso no mantendrá ningún servicio en primer plano.
Ese es exactamente el estado que Android elimina cuando el usuario elimina su aplicación de las recientes.
showNotificationOnPauseOnly se ignora mientras stopOnTerminate: false (#378 ). Las dos configuraciones piden cosas incompatibles y gana la promesa de supervivencia: la notificación permanece visible.
¿Por qué no se puede hacer que funcione?
Cuando se elimina una tarea, ActivityManager decide qué procesos eliminar de proc.foregroundServices y decide antes de enviar onTaskRemoved al subproceso principal de su aplicación. Por lo tanto, un SDK no puede solucionar la situación publicando la notificación en el momento de la eliminación; para entonces el veredicto ya está disponible. Tracelet tenía exactamente esa mitigación y no ayudó.
El resultado fue una carrera que nadie pudo ver. Desliza la aplicación en el espacio entre la salida de la pantalla y la notificación que regresa, y el proceso muere: no hay tareas sin cabeza, no hay eventos, no hay registros, nada de lo que stopOnTerminate: false existe para garantizar. Desliza un segundo después y todo funcionó perfectamente. La brecha se midió en 285 ms en un Pixel Fold y 700-1500 ms en un dispositivo con Android 15.
¿Qué hace Tracelet en su lugar?
| Tu configuración | Comportamiento |
|---|---|
stopOnTerminate: false + showNotificationOnPauseOnly: true | La bandera es ignorada. La notificación permanece visible, el servicio nunca se degrada y un barrido de las últimas noticias nunca puede detener el proceso. |
stopOnTerminate: true + showNotificationOnPauseOnly: true | Funciona exactamente como está documentado: oculto mientras la aplicación está en pantalla. Aquí no se promete nada más allá del golpe, por lo que no hay nada que proteger. |
showNotificationOnPauseOnly: false | La notificación siempre está visible. Este es el valor predeterminado. |
La anulación no es silenciosa; eso fue la mitad de lo que encareció el error original. Se escribe una línea en el canal de registro del ciclo de vida siempre activo por start(), legible con Tracelet.getLogs() en cualquier nivel de registro, incluso desde una ejecución en estado interrumpido:
notification: showNotificationOnPauseOnly ignored because stopOnTerminate=false — hiding the
notification demotes the foreground service, and a swipe from recents in that window kills the
process (#378). Set stopOnTerminate=true to hide it while the app is open, or
showNotificationOnPauseOnly=false to stop asking.getForegroundServiceHealth() informa lo mismo desde el otro lado. Cuando la notificación está legítimamente oculta (stopOnTerminate: true), serviceForeground es false y lastForegroundPromotionResult es suppressed: una degradación que usted solicitó, que es diferente de una promoción que el sistema operativo rechazó.
Eligiendo
Decide cuál necesitas realmente:
- El seguimiento debe sobrevivir al momento en que el usuario desliza la aplicación → mantenga
stopOnTerminate: falsey deje que se muestre la notificación. Este es el valor predeterminado correcto para aplicaciones de entrega, flotas, fuerza laboral y seguridad. - Una bandeja de notificaciones limpia es más importante que sobrevivir al deslizamiento → configura
stopOnTerminate: true. El seguimiento finaliza cuando la aplicación se elimina de las recientes, lo que para muchas aplicaciones de consumo es exactamente lo que el usuario espera de todos modos.
Tenga en cuenta que lo que obtiene en el segundo caso está limitado: en Android 14+, un servicio de ubicación en primer plano debe mostrar su notificación una vez que su aplicación está fuera de la pantalla, por lo que la ocultación solo se aplica mientras el usuario está en su aplicación.
Esto es sólo para Android. iOS no tiene servicio en primer plano, ni notificaciones para ocultar ni eliminación de tareas de este tipo: showNotificationOnPauseOnly vive en AndroidConfig y el SDK de iOS no lo lee.
Mostrar un temporizador transcurrido en la notificación
Para las aplicaciones creadas en torno a una sesión limitada (una entrega, un turno, un trabajo, un viaje), la notificación puede mostrar un reloj en marcha contando desde el momento que usted elija:
final shiftStartedAt = DateTime.now();
await Tracelet.setConfig(
tl.Config(
android: tl.AndroidConfig(
foregroundService: tl.ForegroundServiceConfig(
notificationTitle: 'Shift in progress',
notificationStartedAt: shiftStartedAt.millisecondsSinceEpoch,
notificationShowTimer: true,
notificationOnlyAlertOnce: true,
),
),
),
);
await Tracelet.updateNotification();Android procesa el reloj en sí. Llamas a esto una vez; el sistema operativo marca la pantalla en una segunda resolución durante la vida de la notificación. Su aplicación nunca se activa para volver a dibujarla y volver a publicar la notificación por un motivo no relacionado (texto nuevo, un cambio de configuración) no reinicia el recuento.
Usted proporciona notificationStartedAt en lugar de que Tracelet lo tome de start(), porque el período que le interesa a un usuario a menudo comienza antes que el seguimiento, o sobrevive a un reinicio del seguimiento. Para mostrar “desde que comenzó el seguimiento”, pase el momento en que llamó a start().
Semántica que vale la pena conocer.
notificationShowTimer: false(el valor predeterminado) muestra exactamente la notificación de hoy.notificationShowTimer: truesinnotificationStartedAtno muestra ningún temporizador y registra el motivo. Sustituir “ahora” reiniciaría el reloj en cada reenvío y reportaría erróneamente la sesión.- Un
notificationStartedAten el futuro se fija en la hora actual por publicación, dejando el valor almacenado intacto, por lo que la corrección del reloj del dispositivo se soluciona en la siguiente publicación. - El cronómetro sólo cuenta hacia arriba. Para dejar de mostrarlo, configure
notificationShowTimer: false: el instante de inicio permanece almacenado pero inerte.
Usted puede representar el tiempo transcurrido usted mismo reescribiendo notificationText en un temporizador, y para un recuento estático esa es la herramienta adecuada. La razón para preferir esto es el costo y la resolución: una reescritura por minuto es un viaje completo de ida y vuelta al idioma nativo más una nueva publicación (aproximadamente 480 de ellas en un turno de ocho horas) y mostrar segundos de esa manera no es práctico en absoluto. El cronómetro del sistema operativo es una llamada.
La contraparte de iOS es el temporizador de actividad en vivo, que toma los mismos dos campos en liveActivityConfig.
Escenario 2: La aplicación meteorológica (seguimiento periódico)
Conceptos explorados: WorkManager frente a Exact Alarms
El problema
Su usuario descargó su aplicación meteorológica local. Desea despertarse en segundo plano una vez cada 4 horas, obtener su ubicación y descargar el pronóstico local.
Usted no desea una notificación persistente del Servicio de primer plano. El usuario odiaría ver una notificación permanente de “Seguimiento del tiempo” en su cajón.
Cómo lo resuelve Tracelet: modo periódico
En lugar de un servicio continuo, Tracelet puede utilizar los programadores de trabajos de Android para despertarse brevemente, obtener una solución y volver a dormir.
android: tl.AndroidConfig(
periodicUseForegroundService: false,
periodicUseExactAlarms: true, // Uses AlarmManager instead of WorkManager
)Los dos programadores
-
Administrador de trabajo (
periodicUseExactAlarms: false) El valor predeterminado. Es muy amigable con la batería. Sin embargo, Android decide cuándo ejecutarlo. Si le pide que se ejecute cada 15 minutos, Android podría esperar 45 minutos y ejecutarlo cuando el usuario desbloquee su teléfono para consultar Instagram (lo que se denomina “procesamiento por lotes”). Es muy inexacto. -
Administrador de alarmas (
periodicUseExactAlarms: true) Si es absolutamente NECESARIO activar el dispositivo exactamente a una hora específica, utilice esto. Requiere que declares el permisoSCHEDULE_EXACT_ALARMen tuandroid/app/src/main/AndroidManifest.xml:<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />Nota: Google Play revisa estrictamente este permiso. Úselo solo si su aplicación es un despertador, un calendario o si requiere absolutamente una hora exacta.
Escenario 3: La agresión OEM
Conceptos explorados: API de estado de configuración
El problema
Hiciste todo bien. Tienes un servicio en primer plano. Pero el usuario posee un teléfono Xiaomi. MIUI tiene un “Ahorro de batería” patentado que desactiva incluso los servicios en primer plano después de 5 minutos de tiempo de apagado de la pantalla.
Cómo lo resuelve Tracelet: mitigación automática y avisos
Tracelet aplica automáticamente mitigaciones internas siempre que sea posible, pero a veces el usuario tiene que incluir físicamente su aplicación en la lista blanca en el menú de configuración patentado del OEM.
final health = await tl.Tracelet.getSettingsHealth();
if (health['isAggressiveOem'] == true) {
// Automatically opens the manufacturer-specific settings screen
// (e.g. Xiaomi Autostart, Huawei App Launch, Samsung Sleeping Apps)
await tl.Tracelet.showPowerManager();
}Para ver un ejemplo detallado de cómo implementar esto en su interfaz de usuario de Flutter, consulte la página Herramientas de diagnóstico y administración de energía.