-
Notifications
You must be signed in to change notification settings - Fork 154
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor: Handle ambient mode in Exercise screens
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
Showing
8 changed files
with
512 additions
and
274 deletions.
There are no files selected for viewing
121 changes: 121 additions & 0 deletions
121
...SampleCompose/app/src/main/java/com/example/exercisesamplecompose/ambient/AmbientAware.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
86 changes: 86 additions & 0 deletions
86
...leCompose/app/src/main/java/com/example/exercisesamplecompose/ambient/AmbientAwareTime.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} | ||
} |
83 changes: 83 additions & 0 deletions
83
...SampleCompose/app/src/main/java/com/example/exercisesamplecompose/ambient/AmbientState.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.