Skip to content

Commit

Permalink
Refactor: Handle ambient mode in Exercise screens
Browse files Browse the repository at this point in the history
This commit introduces changes to handle ambient mode within individual exercise screens instead of globally.

- An AmbientAware composable is added to each screen to control ambient behavior.
- Modifiers for ambient mode (ambientGray, ambientBlank) are applied within the screen composables.
- Removed the ambientState parameter from composables and adjusted accordingly.
  • Loading branch information
yschimke committed Dec 6, 2024
1 parent 12ce555 commit c39fca1
Show file tree
Hide file tree
Showing 8 changed files with 512 additions and 274 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.exercisesamplecompose.ambient

import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.wear.ambient.AmbientLifecycleObserver

/**
* Composable for general handling of changes and updates to ambient status. A new
* [AmbientState] is generated with any change of ambient state, as well as with any periodic
* update generated whilst the screen is in ambient mode.
*
* This composable changes the behavior of the activity, enabling Always-On. See:
*
* https://developer.android.com/training/wearables/views/always-on).
*
* It should be used within each individual screen inside nav routes.
*
* @param content Lambda that will be used for building the UI, which is passed the current ambient
* state.
*/
@Composable
fun AmbientAware(
content: @Composable (AmbientState) -> Unit,
) {
// Using AmbientAware correctly relies on there being an Activity context. If there isn't, then
// gracefully allow the composition of [block], but no ambient-mode functionality is enabled.
val activity = LocalContext.current.findActivityOrNull()
val lifecycle = LocalLifecycleOwner.current.lifecycle

var ambientState = remember {
mutableStateOf<AmbientState>(AmbientState.Inactive)
}

val observer = remember {
if (activity != null) {
AmbientLifecycleObserver(
activity,
object : AmbientLifecycleObserver.AmbientLifecycleCallback {
override fun onEnterAmbient(ambientDetails: AmbientLifecycleObserver.AmbientDetails) {
ambientState.value = AmbientState.Ambient(
burnInProtectionRequired = ambientDetails.burnInProtectionRequired,
deviceHasLowBitAmbient = ambientDetails.deviceHasLowBitAmbient,
)
}

override fun onExitAmbient() {
ambientState.value = AmbientState.Interactive
}

override fun onUpdateAmbient() {
val lastAmbientDetails =
(ambientState.value as? AmbientState.Ambient)
ambientState.value = AmbientState.Ambient(
burnInProtectionRequired = lastAmbientDetails?.burnInProtectionRequired == true,
deviceHasLowBitAmbient = lastAmbientDetails?.deviceHasLowBitAmbient == true,
)
}
},
).also { observer ->
ambientState.value =
if (observer.isAmbient) AmbientState.Ambient() else AmbientState.Interactive

lifecycle.addObserver(observer)
}
} else {
null
}
}

val value = ambientState.value
CompositionLocalProvider(LocalAmbientState provides value) {
content(value)
}
}

/**
* AmbientState represents the current state of an ambient effect.
* It defaults to [AmbientState.Inactive] if no state is provided.
*
* @sample
* ```kotlin
* val state = LocalAmbientState.current
* if (state is AmbientState.Active) {
* // Perform actions based on the active state
* }
* ```
*/
val LocalAmbientState = compositionLocalOf<AmbientState> { AmbientState.Inactive }

private fun Context.findActivityOrNull(): Activity? {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.exercisesamplecompose.ambient

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import java.time.ZonedDateTime

/**
* An example of using [AmbientAware]: Provides the time, at the specified update frequency, whilst
* in interactive mode, or when ambient-generated updates occur (typically every 1 min).
*
* Example usage:
*
* AmbientAware { stateUpdate ->
* Box(
* contentAlignment = Alignment.Center,
* modifier = Modifier.fillMaxSize()
* ) {
* AmbientAwareTime(stateUpdate) { dateTime, isAmbient ->
* // Basic example of AmbientAwareTime usage
* val ambientFmt = remember { DateTimeFormatter.ofPattern("HH:mm") }
* val interactiveFmt =
* remember { DateTimeFormatter.ofPattern("HH:mm:ss") }
* val dateTimeStr = if (isAmbient) {
* ambientFmt.format(ZonedDateTime.now())
* } else {
* interactiveFmt.format(ZonedDateTime.now())
* }
* Text(dateTimeStr)
* }
* }
* }
*
* @param stateUpdate The state update from [AmbientAware]
* @param updatePeriodMillis The update period, whilst in interactive mode
* @param block The developer-supplied composable for rendering the date and time.
*/
@Composable
fun AmbientAwareTime(
stateUpdate: AmbientState,
updatePeriodMillis: Long = 1000,
block: @Composable (dateTime: ZonedDateTime, isAmbient: Boolean) -> Unit,
) {
check(updatePeriodMillis >= 1000)

var isAmbient by remember { mutableStateOf(false) }
var currentTime by remember { mutableStateOf<ZonedDateTime?>(null) }

currentTime?.let {
block(it, isAmbient)
}

LaunchedEffect(stateUpdate) {
if (stateUpdate.isInteractive) {
while (isActive) {
isAmbient = false
currentTime = ZonedDateTime.now()
delay(updatePeriodMillis - System.currentTimeMillis() % updatePeriodMillis)
}
} else {
isAmbient = true
currentTime = ZonedDateTime.now()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.exercisesamplecompose.ambient

import androidx.compose.runtime.Immutable
import androidx.wear.ambient.AmbientLifecycleObserver

/**
* Represent Ambient as updates, with the state and time of change. This is necessary to ensure that
* when the system provides a (typically) 1min-frequency callback to onUpdateAmbient, the developer
* may wish to update composables, but the state hasn't changed.
*/
@Immutable
sealed interface AmbientState {
val displayName: String

/**
* Represents that the state of the device is is interactive, and the app is open and being used.
*
* This object is used to track whether the application is currently
* being interacted with by the user.
*/
data object Interactive : AmbientState {
override val displayName: String
get() = "Interactive"
}

/**
* Represents the state of a device, that the app is in ambient mode and not actively updating
* the display.
*
* This class holds information about the ambient display properties, such as
* whether burn-in protection is required, if the device has low bit ambient display,
* and the last time the ambient state was updated.
*
* @see [AmbientLifecycleObserver.AmbientDetails]
* @property burnInProtectionRequired Indicates if burn-in protection is necessary for the device.
* Defaults to false.
* @property deviceHasLowBitAmbient Specifies if the device has a low bit ambient display.
* Defaults to false.
* @property updateTimeMillis The timestamp in milliseconds when the ambient state was last updated.
* Defaults to the current system time.
*/
data class Ambient(
val burnInProtectionRequired: Boolean = false,
val deviceHasLowBitAmbient: Boolean = false,
val updateTimeMillis: Long = System.currentTimeMillis(),
) :
AmbientState {
override val displayName: String
get() = "Ambient"
}

/**
* Represents the state of a device, that the app isn't currently monitoring the ambient state.
*
* @property displayName A user-friendly name for this state, displayed as "Inactive".
*/
data object Inactive : AmbientState {
override val displayName: String
get() = "Inactive"
}

val isInteractive: Boolean
get() = !isAmbient

val isAmbient: Boolean
get() = this is Ambient
}
Loading

0 comments on commit c39fca1

Please sign in to comment.