Apps sempre ativados e modo ambiente do sistema

Este guia explica como deixar o app sempre ativado, como reagir às transições de estado de energia e como gerenciar o comportamento do aplicativo para oferecer uma boa experiência ao usuário e economizar bateria.

Tornar um app constantemente visível afeta significativamente a duração da bateria. Portanto, considere o impacto de energia ao adicionar esse recurso.

Conceitos principais

Quando um app do Wear OS aparece em tela cheia, ele está em um dos dois estados de energia:

  • Interativo: um estado de alta potência em que a tela está com brilho total, permitindo a interação total do usuário.
  • Ambiente: um estado de baixo consumo de energia em que a tela fica escura para economizar energia. Nesse estado, a interface do app ainda ocupa a tela inteira, mas o sistema pode alterar a aparência desfocando ou sobrepondo o conteúdo, como o tempo. Isso também é chamado de modo ambiente.

O sistema operacional controla a transição entre esses estados.

Um app sempre ativado é um aplicativo que mostra conteúdo nos estados interativo e ambiente.

Quando um app sempre ativado continua mostrando a própria interface enquanto o dispositivo está no estado Ambient de baixo consumo de energia, ele é descrito como estando no modo ambiativo.

Transições do sistema e comportamento padrão

Quando um app está em primeiro plano, o sistema gerencia as transições de estado de energia com base em dois tempos limite acionados pela inatividade do usuário.

  • Tempo limite 1: estado interativo para ambiente:após um período de inatividade do usuário, o dispositivo entra no estado Ambiente.
  • Tempo limite 2: retorno ao mostrador do relógio:após um período de inatividade, o sistema pode ocultar o app atual e mostrar o mostrador do relógio.

Imediatamente após o sistema passar pela primeira transição para o estado Ambient, o comportamento padrão depende da versão do Wear OS e da configuração do app:

  • No Wear OS 5 e versões anteriores, o sistema mostra uma captura de tela desfocada do app pausado, com o tempo sobreposto na parte de cima.
  • No Wear OS 6 e mais recentes, se um app for direcionado ao SDK 36 ou mais recente, ele será considerado sempre ativado. A tela fica escura, mas o aplicativo continua executando e permanece visível. As atualizações podem ser tão infrequentes quanto uma por minuto.

Personalizar o comportamento do estado do Modo ambiente

Independentemente do comportamento padrão do sistema, em todas as versões do Wear OS, é possível personalizar a aparência ou o comportamento do app enquanto ele está no estado Ambient usando AmbientLifecycleObserver para detectar callbacks em transições de estado.

Usar AmbientLifecycleObserver

Para reagir a eventos do modo ambiente, use a classe AmbientLifecycleObserver:

  1. Implemente a interface AmbientLifecycleObserver.AmbientLifecycleCallback. Use o método onEnterAmbient() para ajustar a interface ao estado de pouca energia e onExitAmbient() para restaurá-la à tela interativa 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. Crie um AmbientLifecycleObserver e registre-o com o ciclo de vida da atividade ou do elemento combinável.

    private val ambientObserver = AmbientLifecycleObserver(activity, ambientCallback)
    
    override fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        lifecycle.addObserver(ambientObserver)
    
        // ...
    }
    
  3. Chame removeObserver() para remover o observador em onDestroy().

Para desenvolvedores que usam o Jetpack Compose, a biblioteca Horologist oferece um utilitário útil, o elemento combinável AmbientAware, que simplifica a implementação desse padrão.

TimeText com detecção de ambiente

Como exceção à necessidade de um observador personalizado, no Wear OS 6, o widget TimeText é sensível ao ambiente. Ele é atualizado automaticamente uma vez por minuto quando o dispositivo está no estado Ambient sem nenhum código extra.

Controlar a duração da tela ligada

As seções a seguir descrevem como gerenciar o tempo que o app permanece na tela.

Impedir o retorno ao mostrador do relógio com uma atividade em andamento

Após um período no estado Ambient (tempo limite 2), o sistema normalmente retorna ao mostrador do relógio. O usuário pode configurar a duração do tempo limite nas configurações do sistema. Em alguns casos de uso, como um usuário que está rastreando um treino, o app pode precisar permanecer visível por mais tempo.

No Wear OS 5 e em versões mais recentes, é possível evitar isso implementando uma atividade em andamento. Se o app estiver mostrando informações sobre uma tarefa do usuário em andamento, como uma sessão de treino, use a API Ongoing Activity para manter o app visível até o fim da tarefa. Se um usuário retornar manualmente ao mostrador do relógio, o indicador de atividade em andamento vai permitir que ele retorne ao app com um toque.

Para implementar isso, a intent de toque da notificação em andamento precisa apontar para a atividade sempre ativa, conforme mostrado no snippet de código abaixo:

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

Manter a tela ligada e impedir o estado de ambiente

Em casos raros, talvez seja necessário impedir completamente que o dispositivo entre no estado Ambient. Ou seja, para evitar o Timeout 1. Para fazer isso, use a flag de janela FLAG_KEEP_SCREEN_ON. Isso funciona como um bloqueio de ativação, mantendo o dispositivo no estado Interativo. Use com extrema cautela, porque isso afeta significativamente a duração da bateria.

Recomendações para o Modo ambiente

Para oferecer a melhor experiência do usuário e economizar energia no modo Ambient, siga estas diretrizes de design.

  • Usar uma tela minimalista e de baixo consumo de energia
    • Mantenha pelo menos 85% da tela preta.
    • Use contornos para ícones ou botões grandes em vez de preenchimentos sólidos.
    • Mostre apenas as informações mais importantes, movendo os detalhes secundários para a tela interativa.
    • Evite grandes blocos de cores sólidas e branding não funcional ou imagens de plano de fundo.
  • Atualizar o conteúdo de forma adequada
    • Para dados que mudam com frequência, como um cronômetro, a distância do treino ou o tempo, mostre conteúdo de marcador de posição, como --, para evitar a impressão de que o conteúdo é novo.
    • Remova os indicadores de progresso que são atualizados continuamente, como os toques de contagem regressiva e as sessões de mídia.
    • O callback onUpdateAmbient() só deve ser usado para atualizações essenciais, normalmente uma vez por minuto.
  • Manter um layout consistente
    • Mantenha os elementos na mesma posição nos modos Interativo e Ambiente para criar uma transição suave.
    • Mostre sempre a hora.
  • Conhecer o contexto
    • Se o usuário estava em uma tela de configurações ou de configuração quando o dispositivo entrou no modo ambiente, mostre uma tela mais relevante do app em vez da visualização de configurações.
  • Processar requisitos específicos do dispositivo
    • No objeto AmbientDetails transmitido para onEnterAmbient():
      • Se deviceHasLowBitAmbient for true, desative a suavização sempre que possível.
      • Se burnInProtectionRequired for true, mude os elementos da interface periodicamente e evite áreas brancas sólidas para evitar a queima da tela.

Depuração e testes

Estes comandos adb podem ser úteis ao desenvolver ou testar o comportamento do app quando o dispositivo está no 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

Exemplo: app de treino

Considere um app de treino que precisa mostrar métricas para o usuário durante toda a sessão de exercícios. O app precisa permanecer visível durante as transições de estado Ambient e evitar ser substituído pelo mostrador do relógio.

Para fazer isso, o desenvolvedor precisa:

  1. Implemente um AmbientLifecycleObserver para processar mudanças na interface entre os estados Interativo e Ambiente, como escurecer a tela e remover dados não essenciais.
  2. Crie um novo layout de baixo consumo para o estado Ambient que siga as práticas recomendadas.
  3. Use a API Ongoing Activity durante o treino para evitar que o sistema retorne ao mostrador do relógio.

Para uma implementação completa, consulte o exercício de exemplo (link em inglês) baseado no Compose no GitHub. Esse exemplo também demonstra o uso do elemento combinável AmbientAware da biblioteca Horologist para simplificar o processamento do modo ambiente no Compose.