Apps siempre encendidas y modo ambiente del sistema

En esta guía, se explica cómo hacer que tu app esté siempre activa, cómo reaccionar a las transiciones de estado de la batería y cómo administrar el comportamiento de la aplicación para proporcionar una buena experiencia del usuario y, al mismo tiempo, ahorrar batería.

Hacer que una app sea visible constantemente afecta significativamente la duración de batería, por lo que debes tener en cuenta el impacto en la energía cuando agregues esta función.

Conceptos clave

Cuando una app para Wear OS se muestra en pantalla completa, se encuentra en uno de los siguientes dos estados de energía:

  • Interactivo: Es un estado de alta potencia en el que la pantalla está a máxima luminosidad, lo que permite la interacción total del usuario.
  • Ambiente: Es un estado de bajo consumo en el que la pantalla se atenúa para ahorrar batería. En este estado, la IU de tu app aún ocupa toda la pantalla, pero el sistema podría alterar su apariencia desenfocándola o superponiendo contenido, como la hora. Esto también se conoce como Modo ambiente.

El sistema operativo controla la transición entre estos estados.

Una app siempre activa es una aplicación que muestra contenido en los estados Interactivo y Ambiente.

Cuando una app siempre activa sigue mostrando su propia IU mientras el dispositivo está en el estado Ambient de bajo consumo, se describe como en modo ambiactivo.

Transiciones del sistema y comportamiento predeterminado

Cuando una app está en primer plano, el sistema administra las transiciones de estado de energía según dos tiempos de espera activados por la inactividad del usuario.

  • Tiempo de espera n° 1: De estado interactivo a estado ambiente: Después de un período de inactividad del usuario, el dispositivo entra en el estado Ambiente.
  • Tiempo de espera 2: Regreso a la cara de reloj: Después de un período adicional de inactividad, es posible que el sistema oculte la app actual y muestre la cara de reloj.

Inmediatamente después de que el sistema pasa por la primera transición al estado Ambient, el comportamiento predeterminado depende de la versión de Wear OS y de la configuración de tu app:

  • En Wear OS 5 y versiones anteriores, el sistema muestra una captura de pantalla desenfocada de la aplicación pausada, con la hora superpuesta en la parte superior.
  • En Wear OS 6 y versiones posteriores, si una app se orienta al SDK 36 o versiones posteriores, se considera siempre activa. La pantalla se atenúa, pero la aplicación sigue ejecutándose y permanece visible. (Las actualizaciones pueden ser tan poco frecuentes como una vez por minuto).

Cómo personalizar el comportamiento del estado ambiente

Independientemente del comportamiento predeterminado del sistema, en todas las versiones de Wear OS, puedes personalizar el aspecto o el comportamiento de tu app mientras está en el estado Ambient con AmbientLifecycleObserver para escuchar devoluciones de llamada en las transiciones de estado.

Usa AmbientLifecycleObserver

Para reaccionar a los eventos del modo ambiente, usa la clase AmbientLifecycleObserver:

  1. Implementa la interfaz AmbientLifecycleObserver.AmbientLifecycleCallback. Usa el método onEnterAmbient() para ajustar la IU para el estado de baja potencia y onExitAmbient() para restablecerla a la pantalla interactiva completa.

    val ambientCallback = object : AmbientLifecycleObserver.AmbientLifecycleCallback {
        override fun onEnterAmbient(ambientDetails: AmbientLifecycleObserver.AmbientDetails) {
            // ... Called when moving from interactive mode into ambient mode.
            // Adjust UI for low-power state: dim colors, hide non-essential elements.
        }
    
        override fun onExitAmbient() {
            // ... Called when leaving ambient mode, back into interactive mode.
            // Restore full UI.
        }
    
        override fun onUpdateAmbient() {
            // ... Called by the system periodically (typically once per minute)
            // to allow the app to update its display while in ambient mode.
        }
    }
    
  2. Crea un AmbientLifecycleObserver y regístralo con el ciclo de vida de tu actividad o elemento componible.

    private val ambientObserver = AmbientLifecycleObserver(activity, ambientCallback)
    
    override fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        lifecycle.addObserver(ambientObserver)
    
        // ...
    }
    
  3. Llama a removeObserver() para quitar el observador en onDestroy().

Para los desarrolladores que usan Jetpack Compose, la biblioteca de Horologist proporciona una utilidad útil, el elemento componible AmbientAware, que simplifica la implementación de este patrón.

TimeText adaptado al entorno

Como excepción a la necesidad de un observador personalizado, en Wear OS 6, el widget TimeText es consciente del entorno. Se actualiza automáticamente una vez por minuto cuando el dispositivo está en el estado Ambient sin ningún código adicional.

Cómo controlar la duración de la pantalla encendida

En las siguientes secciones, se describe cómo administrar el tiempo que tu app permanece en la pantalla.

Evita que se regrese a la cara de reloj con una actividad en curso

Después de un período en el estado Ambient (tiempo de espera n.° 2), el sistema suele volver a la cara de reloj. El usuario puede configurar la duración del tiempo de espera en la configuración del sistema. En algunos casos de uso, como cuando un usuario hace un seguimiento de un entrenamiento, es posible que una app deba permanecer visible durante más tiempo.

En Wear OS 5 y versiones posteriores, puedes evitar esto implementando una actividad en curso. Si tu app muestra información sobre una tarea en curso del usuario, como una sesión de entrenamiento, puedes usar la API de Ongoing Activity para mantener tu app visible hasta que finalice la tarea. Si un usuario regresa manualmente a la cara de reloj, el indicador de actividad en curso le brinda una forma de volver a tu app con un solo toque.

Para implementar esto, el intent de toque de la notificación en curso debe apuntar a tu actividad siempre activa, como se muestra en el siguiente fragmento de código:

private fun createNotification(): Notification {
    val activityIntent =
        Intent(this, AlwaysOnActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
        }

    val pendingIntent =
        PendingIntent.getActivity(
            this,
            0,
            activityIntent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )

    val notificationBuilder =
        NotificationCompat.Builder(this, CHANNEL_ID)
            // ...
            // ...
            .setOngoing(true)

    // ...

    val ongoingActivity =
        OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder)
            // ...
            // ...
            .setTouchIntent(pendingIntent)
            .build()

    ongoingActivity.apply(applicationContext)

    return notificationBuilder.build()
}

Cómo mantener la pantalla encendida y evitar el estado ambiente

En casos excepcionales, es posible que debas evitar por completo que el dispositivo entre en el estado Ambient. Es decir, para evitar el tiempo de espera n.° 1. Para ello, puedes usar la marca de ventana FLAG_KEEP_SCREEN_ON. Esto funciona como un bloqueo de activación, lo que mantiene el dispositivo en el estado Interactivo. Usa esta función con extrema precaución, ya que afecta en gran medida la duración de la batería.

Recomendaciones para el Modo ambiente

Para proporcionar la mejor experiencia del usuario y conservar energía en el modo Ambient, sigue estos lineamientos de diseño.

  • Usa una pantalla minimalista y de bajo consumo
    • Mantén al menos el 85% de la pantalla en negro.
    • Usa contornos para íconos o botones grandes en lugar de rellenos sólidos.
    • Muestra solo la información más importante y mueve los detalles secundarios a la pantalla interactiva.
    • Evita los grandes bloques de color sólido y el desarrollo de la marca o las imágenes de fondo no funcionales.
  • Asegúrate de que el contenido se actualice de forma adecuada
    • Para los datos que cambian con frecuencia, como un cronómetro, la distancia o el tiempo del entrenamiento, muestra contenido de marcador de posición, como --, para evitar dar la impresión de que el contenido es reciente.
    • Quita los indicadores de progreso que se actualizan de forma continua, como los anillos de cuenta regresiva y las sesiones multimedia.
    • La devolución de llamada de onUpdateAmbient() solo debe usarse para actualizaciones esenciales, por lo general, una vez por minuto.
  • Mantén un diseño coherente
    • Mantén los elementos en la misma posición en los modos Interactivo y Ambiente para crear una transición fluida.
    • Mostrar siempre la hora
  • Ten en cuenta el contexto
    • Si el usuario estaba en una pantalla de configuración cuando el dispositivo entra en modo ambiente, considera mostrar una pantalla más relevante de tu app en lugar de la vista de configuración.
  • Controla los requisitos específicos del dispositivo
    • En el objeto AmbientDetails que se pasa a onEnterAmbient(), haz lo siguiente:
      • Si deviceHasLowBitAmbient es true, inhabilita el suavizado de contorno siempre que sea posible.
      • Si burnInProtectionRequired es true, cambia los elementos de la IU de forma periódica y evita las áreas blancas sólidas para evitar el efecto quemado de la pantalla.

Depuración y pruebas

Estos comandos adb pueden ser útiles cuando desarrollas o pruebas el comportamiento de tu app cuando el dispositivo está en modo ambiente:

# put device in ambient mode if the always on display is enabled in settings
# (and not disabled by other settings, such as theatre mode)
$ adb shell input keyevent KEYCODE_SLEEP

# put device in interactive mode
$ adb shell input keyevent KEYCODE_WAKEUP

Ejemplo: Aplicación de entrenamiento

Considera una app de entrenamiento que necesita mostrarle métricas al usuario durante toda la duración de su sesión de ejercicio. La app debe permanecer visible a través de las transiciones de estado Ambient y evitar que la cara de reloj la reemplace.

Para lograrlo, el desarrollador debe hacer lo siguiente:

  1. Implementa un AmbientLifecycleObserver para controlar los cambios de la IU entre los estados interactivo y ambiente, como atenuar la pantalla y quitar los datos no esenciales.
  2. Crea un nuevo diseño de bajo consumo para el estado Ambient que siga las prácticas recomendadas.
  3. Usa la API de Ongoing Activity durante el entrenamiento para evitar que el sistema vuelva a la cara de reloj.

Para ver una implementación completa, consulta el ejemplo de ejercicio basado en Compose en GitHub. En este ejemplo, también se muestra el uso del elemento componible AmbientAware de la biblioteca Horologist para simplificar el manejo del modo ambiente en Compose.