Always-on apps and system ambient mode

This guide explains how to make your app always-on, how to react to power state transitions, and how to manage application behavior to provide a good user experience while conserving battery.

Making an app constantly visible significantly impacts battery life, so consider the power impact when adding this feature.

Key Concepts

When a Wear OS app is displayed on the full screen, it is in one of two power states:

  • Interactive: A high-power state where the screen is at full brightness, allowing for full user interaction.
  • Ambient: A low-power state where the display dims to conserve power. In this state, your app's UI still occupies the full screen, but the system might alter its appearance by blurring it or overlaying content like the time. This is also referred to as Ambient Mode.

The operating system controls the transition between these states.

An Always-On App is an application that displays content in both the Interactive and Ambient states.

When an always-on app continues to display its own UI while the device is in the low-power Ambient state, it is described as being in ambiactive mode.

System transitions and default behavior

When an app is in the foreground, the system manages power state transitions based on two timeouts triggered by user inactivity.

  • Timeout #1: Interactive to Ambient state: After a period of user inactivity, the device enters the Ambient state.
  • Timeout #2: Return to watch face: After a further period of inactivity, the system may hide the current app and display the watch face.

Immediately after the system passes through the first transition into the Ambient state, the default behavior depends on the Wear OS version and your app's configuration:

  • On Wear OS 5 and lower, the system displays a blurred screenshot of your paused application, with the time overlaid on top.
  • On Wear OS 6 and higher, if an app targets SDK 36 or newer, it is considered always-on. The display is dimmed, but the application continues running and remains visible. (Updates may be as infrequent as once per minute.)

Customize behavior for the Ambient state

Regardless of the default system behavior, on all Wear OS versions you can customize your app's appearance or behavior while in the Ambient state by using AmbientLifecycleObserver to listen for callbacks on state transitions.

Use AmbientLifecycleObserver

To react to ambient mode events, use the AmbientLifecycleObserver class:

  1. Implement the AmbientLifecycleObserver.AmbientLifecycleCallback interface. Use the onEnterAmbient() method to adjust your UI for the low-power state, and onExitAmbient() to restore it to the full interactive display.

    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. Create an AmbientLifecycleObserver and register it with the lifecycle of your activity or composable.

    private val ambientObserver = AmbientLifecycleObserver(activity, ambientCallback)
    
    override fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        lifecycle.addObserver(ambientObserver)
    
        // ...
    }
    
  3. Call removeObserver() to remove the observer in onDestroy().

For developers using Jetpack Compose, the Horologist library provides a helpful utility, the AmbientAware composable, which simplifies the implementation of this pattern.

Ambient-aware TimeText

As an exception to requiring a custom observer, on Wear OS 6 the TimeText widget is ambient-aware. It automatically updates once per minute when the device is in the Ambient state without any additional code.

Control screen-on duration

The following sections describe how to manage how long your app stays on the screen.

Prevent returning to the watch face with an Ongoing Activity

After a period of time in the Ambient state (Timeout #2), the system will typically return to the watch face. The user can configure the timeout duration in the system settings. For certain use cases, like a user tracking a workout, an app might need to remain visible for longer.

On Wear OS 5 and higher, you can prevent this by implementing an Ongoing Activity. If your app is displaying information about an ongoing user task, such as a workout session, you can use the Ongoing Activity API to keep your app visible until the task ends. If a user manually returns to the watchface, the ongoing activity indicator provides one-tap way for them to return to your app

To implement this, the ongoing notification's touch intent must point to your always-on activity, as shown in the following code snippet:

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

Keep the screen on and prevent Ambient state

In rare cases, you might need to completely prevent the device from entering the Ambient state. That is, to avoid Timeout #1. To do this, you can use the FLAG_KEEP_SCREEN_ON window flag. This functions as a wake lock, keeping the device in the Interactive state. Use this with extreme caution as it severely impacts battery life.

Recommendations for Ambient mode

To provide the best user experience and conserve power in Ambient mode, follow these design guidelines.

  • Use a minimalist, low-power display
    • Keep at least 85% of the screen black.
    • Use outlines for large icons or buttons rather than solid fills.
    • Show only the most critical information, moving secondary details to the interactive display.
    • Avoid large blocks of solid color and non-functional branding or background images.
  • Ensure content updates appropriately
    • For frequently changing data like a stopwatch, workout distance, or time, show placeholder content like -- to avoid giving the impression that content is fresh.
    • Remove progress indicators that update continuously, such as for countdown rings and media sessions.
    • The onUpdateAmbient() callback should only be used for essential updates, typically once per minute.
  • Maintain a consistent layout
    • Keep elements in the same position across Interactive and Ambient modes to create a smooth transition.
    • Always show the time.
  • Be context aware
    • If the user was on a settings or configuration screen when the device enters ambient mode, consider showing a more relevant screen from your app instead of the settings view.
  • Handle device-specific requirements
    • In the AmbientDetails object passed to onEnterAmbient():
      • If deviceHasLowBitAmbient is true, disable anti-aliasing where possible.
      • If burnInProtectionRequired is true, periodically shift the UI elements slightly and avoid solid white areas to prevent screen burn-in.

Debugging and testing

These adb commands may be useful when developing or testing how your app behaves when the device is in ambient mode:

# 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

Example: Workout app

Consider a workout app that needs to display metrics to the user for the entire duration of their exercise session. The app must remain visible through Ambient state transitions and avoid being replaced by the watch face.

To achieve this, the developer should do the following:

  1. Implement an AmbientLifecycleObserver to handle UI changes between Interactive and Ambient states, such as dimming the screen and removing non-essential data.
  2. Create a new low-powered layout for the Ambient state that follows the best practices.
  3. Use the Ongoing Activity API for the duration of the workout to prevent the system from returning to the watch face.

For a complete implementation, see the compose-based Exercise sample on GitHub. This sample also demonstrates the use of the AmbientAware composable from the Horologist library to simplify ambient mode handling in Compose.