diff --git a/README.md b/README.md index 3798c75..559bda2 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,6 @@ This library provides a convenience wrapper over the [Sensor APIs](https://devel ## Demo ❤️ ![Screen Recording 2023-04-03 at 1 00 08 PM](https://user-images.githubusercontent.com/89389061/229441943-6339d18f-c704-4d92-9fe8-28c2fd94fdeb.gif) -## WIP 🚧 -This library is a work-in-progress and is subject to major changes. Our team is working hard to get it stable as soon as possible. Thank you for your patience 🌺 - ## Usage 🚀 ### Install dependency 📲 #### Kotlin `build.gradle.kts (:module-name)` @@ -65,15 +62,15 @@ Ambient Temperature | ✅️ | rememberAmbientTemperatureSensorState() Magnetic Field (Uncalibrated) | ✅️️ | rememberUncalibratedMagneticFieldSensorState() GameRotation Vector | ✅️ | rememberGameRotationVectorSensorState() Gyroscope (Uncalibrated) | ✅️ | rememberUncalibratedGyroscopeSensorState() -Significant Motion | — | N/A +Significant Motion | ✅ | rememberSignificantMotionSensorState(onMotionEvent = {}) Step Detector | ✅️ | rememberStepDetectorSensorState() Step Counter | ✅️ | rememberStepCounterSensorState() Geomagnetic Rotation Vector | ✅️️ | rememberGeomagneticRotationVectorSensorState() Heart Rate | ✅️ | rememberHeartRateSensorState() Pose6DOF | — | N/A -Stationary Detect | ⚠️ | WIP -Motion Detect | ⚠️ | WIP -Heart Beat | — | N/A +Stationary Detect | ✅️ | rememberStationaryDetectSensorState() +Motion Detect | ✅️ | rememberMotionDetectSensorState() +Heart Beat | ✅ | rememberHeartBeatSensorState() Low Latency Off-Body Detect | ✅️ | rememberLowLatencyOffBodyDetectSensorState() Accelerometer (Uncalibrated) | ✅️ | rememberUncalibratedAccelerometerSensorState() Hinge Angle | ✅️ | rememberHingeAngleSensorState() diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/AccelerometerSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/AccelerometerSensorState.kt index 42c8758..38150c6 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/AccelerometerSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/AccelerometerSensorState.kt @@ -22,7 +22,9 @@ class AccelerometerSensorState internal constructor( val zForce: Float = 0f, val isAvailable: Boolean = false, val accuracy: Int = 0, -) { + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is AccelerometerSensorState) return false @@ -32,6 +34,8 @@ class AccelerometerSensorState internal constructor( if (zForce != other.zForce) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -42,30 +46,44 @@ class AccelerometerSensorState internal constructor( result = 31 * result + zForce.hashCode() result = 31 * result + isAvailable.hashCode() result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } override fun toString(): String { return "AccelerometerSensorState(xForce=$xForce, yForce=$yForce, zForce=$zForce, " + - "isAvailable=$isAvailable, accuracy=$accuracy)" + "isAvailable=$isAvailable, accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() } } /** * Creates and [remember]s an instance of [AccelerometerSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberAccelerometerSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, - onError: (throwable: Throwable) -> Unit = {}, + onError: (throwable: Throwable) -> Unit = {} ): AccelerometerSensorState { val sensorState = rememberSensorState( sensorType = SensorType.Accelerometer, sensorDelay = sensorDelay, - onError = onError, + autoStart = autoStart, + onError = onError ) val accelerometerSensorState = remember { mutableStateOf(AccelerometerSensorState()) } @@ -79,7 +97,9 @@ fun rememberAccelerometerSensorState( yForce = sensorStateValues[1], zForce = sensorStateValues[2], isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/AmbientTemperatureSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/AmbientTemperatureSensorState.kt index 0f9f63c..96f9ebd 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/AmbientTemperatureSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/AmbientTemperatureSensorState.kt @@ -14,63 +14,84 @@ import androidx.compose.runtime.remember */ @Immutable class AmbientTemperatureSensorState internal constructor( - val temperature: Float = 0f, - val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { - override fun toString(): String { - return "AmbientTemperatureSensorState(temperature=$temperature, " + - "isAvailable=$isAvailable, accuracy=$accuracy)" - } + val temperature: Float = 0f, + val isAvailable: Boolean = false, + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { + override fun equals(other: Any?): Boolean { + if (this === other) return true + other as AmbientTemperatureSensorState - override fun equals(other: Any?): Boolean { - if (this === other) return true - other as AmbientTemperatureSensorState + if (temperature != other.temperature) return false + if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false + return true + } - if (temperature != other.temperature) return false - if (isAvailable != other.isAvailable) return false - if (accuracy != other.accuracy) return false - return true - } + override fun hashCode(): Int { + var result = temperature.hashCode() + result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() + return result + } - override fun hashCode(): Int { - var result = temperature.hashCode() - result = 31 * result + isAvailable.hashCode() - result = 31 * result + accuracy - return result - } -} + override fun toString(): String { + return "AmbientTemperatureSensorState(temperature=$temperature, " + + "isAvailable=$isAvailable, accuracy=$accuracy)" + } + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } +} /** * Creates and [remember]s instance of [AmbientTemperatureSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberAmbientTemperatureSensorState( - sensorDelay: SensorDelay = SensorDelay.Normal, - onError: (throwable: Throwable) -> Unit = {}, + autoStart: Boolean = true, + sensorDelay: SensorDelay = SensorDelay.Normal, + onError: (throwable: Throwable) -> Unit = {} ): AmbientTemperatureSensorState { - val sensorState = rememberSensorState( - sensorType = SensorType.AmbientTemperature, - sensorDelay = sensorDelay, - onError = onError, - ) - - val ambientTemperatureSensorState = remember { mutableStateOf(AmbientTemperatureSensorState()) } + val sensorState = rememberSensorState( + sensorType = SensorType.AmbientTemperature, + sensorDelay = sensorDelay, + autoStart = autoStart, + onError = onError + ) - LaunchedEffect(key1 = sensorState, block = { - val sensorStateValues = sensorState.data - if (sensorStateValues.isNotEmpty()) { - ambientTemperatureSensorState.value = AmbientTemperatureSensorState( - temperature = sensorStateValues[0], - isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy - ) + val ambientTemperatureSensorState = remember { + mutableStateOf(AmbientTemperatureSensorState()) } - }) - return ambientTemperatureSensorState.value -} \ No newline at end of file + LaunchedEffect(key1 = sensorState, block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + ambientTemperatureSensorState.value = AmbientTemperatureSensorState( + temperature = sensorStateValues[0], + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening + ) + } + }) + + return ambientTemperatureSensorState.value +} diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/GameRotationVectorSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/GameRotationVectorSensorState.kt index 6d5c83f..db1f1f9 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/GameRotationVectorSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/GameRotationVectorSensorState.kt @@ -16,72 +16,93 @@ import androidx.compose.runtime.remember */ @Immutable class GameRotationVectorSensorState internal constructor( - val vectorX: Float = 0f, - val vectorY: Float = 0f, - val vectorZ: Float = 0f, - val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { - override fun toString(): String { - return "GameRotationVectorSensorState(vectorX=$vectorX, " + - "vectorY=$vectorY, vectorZ=$vectorZ, isAvailable=$isAvailable, " + - "accuracy=$accuracy)" - } + val vectorX: Float = 0f, + val vectorY: Float = 0f, + val vectorZ: Float = 0f, + val isAvailable: Boolean = false, + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { + override fun equals(other: Any?): Boolean { + if (this === other) return true + other as GameRotationVectorSensorState - override fun equals(other: Any?): Boolean { - if (this === other) return true - other as GameRotationVectorSensorState + if (vectorX != other.vectorX) return false + if (vectorY != other.vectorY) return false + if (vectorZ != other.vectorZ) return false + if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false + return true + } - if (vectorX != other.vectorX) return false - if (vectorY != other.vectorY) return false - if (vectorZ != other.vectorZ) return false - if (isAvailable != other.isAvailable) return false - if (accuracy != other.accuracy) return false - return true - } + override fun hashCode(): Int { + var result = vectorX.hashCode() + result = 31 * result + vectorY.hashCode() + result = 31 * result + vectorZ.hashCode() + result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() + return result + } - override fun hashCode(): Int { - var result = vectorX.hashCode() - result = 31 * result + vectorY.hashCode() - result = 31 * result + vectorZ.hashCode() - result = 31 * result + isAvailable.hashCode() - result = 31 * result + accuracy - return result - } -} + override fun toString(): String { + return "GameRotationVectorSensorState(vectorX=$vectorX, " + + "vectorY=$vectorY, vectorZ=$vectorZ, isAvailable=$isAvailable, " + + "accuracy=$accuracy)" + } + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } +} /** * Creates and [remember]s instance of [GameRotationVectorSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Game]. * @param onError Callback invoked on every error state. */ @Composable fun rememberGameRotationVectorSensorState( - sensorDelay: SensorDelay = SensorDelay.Game, - onError: (throwable: Throwable) -> Unit = {}, + autoStart: Boolean = true, + sensorDelay: SensorDelay = SensorDelay.Game, + onError: (throwable: Throwable) -> Unit = {} ): GameRotationVectorSensorState { - val sensorState = rememberSensorState( - sensorType = SensorType.GameRotationVector, - sensorDelay = sensorDelay, - onError = onError, - ) - - val gameRotationVectorSensorState = remember { mutableStateOf(GameRotationVectorSensorState()) } + val sensorState = rememberSensorState( + sensorType = SensorType.GameRotationVector, + sensorDelay = sensorDelay, + autoStart = autoStart, + onError = onError + ) - LaunchedEffect(key1 = sensorState, block = { - val sensorStateValues = sensorState.data - if (sensorStateValues.isNotEmpty()) { - gameRotationVectorSensorState.value = GameRotationVectorSensorState( - vectorX = sensorStateValues[0], - vectorY = sensorStateValues[1], - vectorZ = sensorStateValues[2], - isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy - ) + val gameRotationVectorSensorState = remember { + mutableStateOf(GameRotationVectorSensorState()) } - }) - return gameRotationVectorSensorState.value -} \ No newline at end of file + LaunchedEffect(key1 = sensorState, block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + gameRotationVectorSensorState.value = GameRotationVectorSensorState( + vectorX = sensorStateValues[0], + vectorY = sensorStateValues[1], + vectorZ = sensorStateValues[2], + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening + ) + } + }) + + return gameRotationVectorSensorState.value +} diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/GeomagneticRotationVectorSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/GeomagneticRotationVectorSensorState.kt index 4f6c8b8..bb99d6e 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/GeomagneticRotationVectorSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/GeomagneticRotationVectorSensorState.kt @@ -16,73 +16,93 @@ import androidx.compose.runtime.remember */ @Immutable class GeomagneticRotationVectorSensorState internal constructor( - val vectorX: Float = 0f, - val vectorY: Float = 0f, - val vectorZ: Float = 0f, - val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { - override fun toString(): String { - return "GeomagneticRotationVectorSensorState(vectorX=$vectorX, " + - "vectorY=$vectorY, vectorZ=$vectorZ, isAvailable=$isAvailable, " + - "accuracy=$accuracy)" - } + val vectorX: Float = 0f, + val vectorY: Float = 0f, + val vectorZ: Float = 0f, + val isAvailable: Boolean = false, + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { + override fun equals(other: Any?): Boolean { + if (this === other) return true + other as GeomagneticRotationVectorSensorState - override fun equals(other: Any?): Boolean { - if (this === other) return true - other as GeomagneticRotationVectorSensorState + if (vectorX != other.vectorX) return false + if (vectorY != other.vectorY) return false + if (vectorZ != other.vectorZ) return false + if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false - if (vectorX != other.vectorX) return false - if (vectorY != other.vectorY) return false - if (vectorZ != other.vectorZ) return false - if (isAvailable != other.isAvailable) return false - if (accuracy != other.accuracy) return false + return true + } - return true - } + override fun hashCode(): Int { + var result = vectorX.hashCode() + result = 31 * result + vectorY.hashCode() + result = 31 * result + vectorZ.hashCode() + result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() + return result + } - override fun hashCode(): Int { - var result = vectorX.hashCode() - result = 31 * result + vectorY.hashCode() - result = 31 * result + vectorZ.hashCode() - result = 31 * result + isAvailable.hashCode() - result = 31 * result + accuracy - return result - } + override fun toString(): String { + return "GeomagneticRotationVectorSensorState(vectorX=$vectorX, " + + "vectorY=$vectorY, vectorZ=$vectorZ, isAvailable=$isAvailable, " + + "accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } } /** * Creates and [remember]s instance of [GeomagneticRotationVectorSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberGeomagneticRotationVectorSensorState( - sensorDelay: SensorDelay = SensorDelay.Normal, - onError: (throwable: Throwable) -> Unit = {}, + autoStart: Boolean = true, + sensorDelay: SensorDelay = SensorDelay.Normal, + onError: (throwable: Throwable) -> Unit = {} ): GeomagneticRotationVectorSensorState { - val sensorState = rememberSensorState( - sensorType = SensorType.GeomagneticRotationVector, - sensorDelay = sensorDelay, - onError = onError, - ) + val sensorState = rememberSensorState( + sensorType = SensorType.GeomagneticRotationVector, + sensorDelay = sensorDelay, + autoStart = autoStart, + onError = onError + ) - val geomagneticRotationVectorSensorState = - remember { mutableStateOf(GeomagneticRotationVectorSensorState()) } + val geomagneticRotationVectorSensorState = + remember { mutableStateOf(GeomagneticRotationVectorSensorState()) } - LaunchedEffect(key1 = sensorState, block = { - val sensorStateValues = sensorState.data - if (sensorStateValues.isNotEmpty()) { - geomagneticRotationVectorSensorState.value = GeomagneticRotationVectorSensorState( - vectorX = sensorStateValues[0], - vectorY = sensorStateValues[1], - vectorZ = sensorStateValues[2], - isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy - ) - } - }) + LaunchedEffect(key1 = sensorState, block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + geomagneticRotationVectorSensorState.value = GeomagneticRotationVectorSensorState( + vectorX = sensorStateValues[0], + vectorY = sensorStateValues[1], + vectorZ = sensorStateValues[2], + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening + ) + } + }) - return geomagneticRotationVectorSensorState.value -} \ No newline at end of file + return geomagneticRotationVectorSensorState.value +} diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/GravitySensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/GravitySensorState.kt index 96e2017..5a71b1b 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/GravitySensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/GravitySensorState.kt @@ -22,7 +22,9 @@ class GravitySensorState internal constructor( val zForce: Float = 0f, val isAvailable: Boolean = false, val accuracy: Int = 0, -) { + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is GravitySensorState) return false @@ -31,6 +33,9 @@ class GravitySensorState internal constructor( if (yForce != other.yForce) return false if (zForce != other.zForce) return false if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -40,30 +45,45 @@ class GravitySensorState internal constructor( result = 31 * result + yForce.hashCode() result = 31 * result + zForce.hashCode() result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } override fun toString(): String { return "GravitySensorState(xForce=$xForce, yForce=$yForce, " + - "zForce=$zForce, isAvailable=$isAvailable, accuracy=$accuracy)" + "zForce=$zForce, isAvailable=$isAvailable, accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() } } /** * Creates and [remember]s an instance of [GravitySensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberGravitySensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, - onError: (throwable: Throwable) -> Unit = {}, + onError: (throwable: Throwable) -> Unit = {} ): GravitySensorState { val sensorState = rememberSensorState( sensorType = SensorType.Gravity, sensorDelay = sensorDelay, - onError = onError, + autoStart = autoStart, + onError = onError ) val gravitySensorState = remember { mutableStateOf(GravitySensorState()) } @@ -77,7 +97,9 @@ fun rememberGravitySensorState( yForce = sensorStateValues[1], zForce = sensorStateValues[2], isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/GyroscopeSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/GyroscopeSensorState.kt index d6d268e..fde8d26 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/GyroscopeSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/GyroscopeSensorState.kt @@ -22,7 +22,9 @@ class GyroscopeSensorState internal constructor( val zRotation: Float = 0f, val isAvailable: Boolean = false, val accuracy: Int = 0, -) { + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is GyroscopeSensorState) return false @@ -31,6 +33,8 @@ class GyroscopeSensorState internal constructor( if (yRotation != other.yRotation) return false if (zRotation != other.zRotation) return false if (isAvailable != other.isAvailable) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -40,30 +44,44 @@ class GyroscopeSensorState internal constructor( result = 31 * result + yRotation.hashCode() result = 31 * result + zRotation.hashCode() result = 31 * result + isAvailable.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } override fun toString(): String { return "GyroscopeSensorState(xRotation=$xRotation, yRotation=$yRotation, " + - "zRotation=$zRotation, isAvailable=$isAvailable, accuracy=$accuracy)" + "zRotation=$zRotation, isAvailable=$isAvailable, accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() } } /** * Creates and [remember]s an instance of [GyroscopeSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberGyroscopeSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, - onError: (throwable: Throwable) -> Unit = {}, + onError: (throwable: Throwable) -> Unit = {} ): GyroscopeSensorState { val sensorState = rememberSensorState( sensorType = SensorType.Gyroscope, sensorDelay = sensorDelay, - onError = onError, + autoStart = autoStart, + onError = onError ) val gyroscopeSensorState = remember { mutableStateOf(GyroscopeSensorState()) } @@ -77,7 +95,9 @@ fun rememberGyroscopeSensorState( yRotation = sensorStateValues[1], zRotation = sensorStateValues[2], isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/HeadTrackerSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/HeadTrackerSensorState.kt index 507caef..0529503 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/HeadTrackerSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/HeadTrackerSensorState.kt @@ -9,13 +9,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember /** - * The HeadTracker sensor measures the orientation of a user's head relative to an arbitrary reference frame, as well as the rate of rotation. + * The HeadTracker sensor measures the orientation of a user's head relative to an arbitrary + * reference frame, as well as the rate of rotation. * @param xRotation X component of Euler vector representing rotation. Defaults to 0f. * @param yRotation Y component of Euler vector representing rotation. Defaults to 0f. * @param zRotation Z component of Euler vector representing rotation. Defaults to 0f. - * @param xAngularVelocity X component of Euler vector representing angular velocity (if supported, otherwise 0). Defaults to 0f. - * @param yAngularVelocity Y component of Euler vector representing angular velocity (if supported, otherwise 0). Defaults to 0f. - * @param zAngularVelocity Z component of Euler vector representing angular velocity (if supported, otherwise 0). Defaults to 0f. + * @param xAngularVelocity X component of Euler vector representing angular velocity (if supported, + * otherwise 0). Defaults to 0f. + * @param yAngularVelocity Y component of Euler vector representing angular velocity (if supported, + * otherwise 0). Defaults to 0f. + * @param zAngularVelocity Z component of Euler vector representing angular velocity (if supported, + * otherwise 0). Defaults to 0f. * @param isAvailable Whether the current device has a gyroscope sensor. Defaults to false. * @param accuracy Accuracy factor of the gyroscope sensor. Defaults to 0. **/ @@ -28,8 +32,10 @@ class HeadTrackerSensorState internal constructor( val yAngularVelocity: Float = 0f, val zAngularVelocity: Float = 0f, val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -45,6 +51,8 @@ class HeadTrackerSensorState internal constructor( if (zAngularVelocity != other.zAngularVelocity) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -57,17 +65,32 @@ class HeadTrackerSensorState internal constructor( result = 31 * result + yAngularVelocity.hashCode() result = 31 * result + zAngularVelocity.hashCode() result = 31 * result + isAvailable.hashCode() - result = 31 * result + accuracy + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } override fun toString(): String { - return "HeadTrackerSensorState(xRotation=$xRotation, yRotation=$yRotation, zRotation=$zRotation, xAngularVelocity=$xAngularVelocity, yAngularVelocity=$yAngularVelocity, zAngularVelocity=$zAngularVelocity, isAvailable=$isAvailable, accuracy=$accuracy)" + return "HeadTrackerSensorState(xRotation=$xRotation, yRotation=$yRotation," + + " zRotation=$zRotation, xAngularVelocity=$xAngularVelocity," + + " yAngularVelocity=$yAngularVelocity, zAngularVelocity=$zAngularVelocity," + + " isAvailable=$isAvailable, accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() } } /** * Creates and [remember]s an instance of [HeadTrackerSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. @@ -75,12 +98,14 @@ class HeadTrackerSensorState internal constructor( @RequiresApi(Build.VERSION_CODES.TIRAMISU) @Composable fun rememberHeadTrackerSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, onError: (throwable: Throwable) -> Unit = {} ): HeadTrackerSensorState { val sensorState = rememberSensorState( sensorType = SensorType.HeadTracker, sensorDelay = sensorDelay, + autoStart = autoStart, onError = onError ) @@ -97,7 +122,9 @@ fun rememberHeadTrackerSensorState( yAngularVelocity = sensorStateValues[4], zAngularVelocity = sensorStateValues[5], isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } }) diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/HeadingSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/HeadingSensorState.kt index bbebb33..e1a9c50 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/HeadingSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/HeadingSensorState.kt @@ -9,73 +9,99 @@ import androidx.compose.runtime.remember /** * Measures a device's heading value in degrees (anti-clockwise), * - * @param degrees Indicates direction in which the device is pointing - * relative to true north in degrees. Defaults to 0f. - * @param accuracy Indicates the confidence of prediction, under Gaussian - * standard normal distribution. Defaults to 0f. - * @param isAvailable Whether the current device has a heading sensor. - * Defaults to false. + * @param degrees Indicates direction in which the device is pointing relative to true north in + * degrees. Defaults to 0f. + * @param headingAccuracy Indicates the confidence of prediction, under Gaussian standard normal + * distribution. Defaults to 0f. + * @param isAvailable Whether the current device has a heading sensor. Defaults to false. + * @param accuracy Accuracy factor of the heading sensor. Defaults to 0. */ @Immutable class HeadingSensorState internal constructor( val degrees: Float = 0f, - val accuracy: Float = 0f, - val isAvailable: Boolean = false -) { + val headingAccuracy: Float = 0f, + val isAvailable: Boolean = false, + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is HeadingSensorState) return false + + if (degrees != other.degrees) return false + if (headingAccuracy != other.headingAccuracy) return false + if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false + + return true + } + override fun hashCode(): Int { var result = degrees.hashCode() - result = 31 * result + accuracy.hashCode() + result = 31 * result + headingAccuracy.hashCode() result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } override fun toString(): String { - return "HeadingSensorState(degrees=$degrees, accuracy=$accuracy, " + - "isAvailable=$isAvailable)" + return "HeadingSensorState(degrees=$degrees, headingAccuracy=$headingAccuracy, " + + "isAvailable=$isAvailable, accuracy=$accuracy)" } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is HeadingSensorState) return false - - if (degrees != other.degrees) return false - if (accuracy != other.accuracy) return false - if (isAvailable != other.isAvailable) return false + override fun startListening() { + startListeningEvents?.invoke() + } - return true + override fun stopListening() { + stopListeningEvents?.invoke() } } /** * Creates and [remember]s an instance of [HeadingSensorState]. - * - * @param sensorDelay The rate at which the raw sensor data should be - * received. Defaults to [SensorDelay.Normal]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. + * @param sensorDelay The rate at which the raw sensor data should be received. Defaults to + * [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberHeadingSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, onError: (throwable: Throwable) -> Unit = {} ): HeadingSensorState { val sensorState = rememberSensorState( sensorType = SensorType.Heading, sensorDelay = sensorDelay, + autoStart = autoStart, onError = onError ) val headingSensorState = remember { mutableStateOf(HeadingSensorState()) } - LaunchedEffect(key1 = sensorState, block = { - val sensorStateValues = sensorState.data - if (sensorStateValues.isNotEmpty()) { - headingSensorState.value = HeadingSensorState( - degrees = sensorStateValues[0], - accuracy = sensorStateValues[1], - isAvailable = sensorState.isAvailable - ) + LaunchedEffect( + key1 = sensorState, + block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + headingSensorState.value = HeadingSensorState( + degrees = sensorStateValues[0], + headingAccuracy = sensorStateValues[1], + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening + ) + } } - }) + ) return headingSensorState.value } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/HeartBeatSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/HeartBeatSensorState.kt new file mode 100644 index 0000000..0b28145 --- /dev/null +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/HeartBeatSensorState.kt @@ -0,0 +1,109 @@ +package com.mutualmobile.composesensors + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember + +/** + * The HeartBeat sensor returns an event everytime a heart beat peak is detected. Peak here ideally + * corresponds to the positive peak in the QRS complex of an ECG signal. + * + * For more info, please refer the [Android Documentation Reference](https://developer.android.com/reference/android/hardware/SensorEvent#sensor.type_heart_beat:) + * + * @param isConfidentPeak A confidence value of 0.0 (false) indicates complete uncertainty - that a peak + * is as likely to be at the indicated timestamp as anywhere else. A confidence value of 1.0 (true) + * indicates complete certainly - that a peak is completely unlikely to be anywhere else on the QRS + * complex. Defaults to false. + * @param isAvailable Whether the current device has a heart beat sensor. Defaults to false. + * @param accuracy Accuracy factor of the heart beat sensor. Defaults to 0. + */ +@RequiresApi(Build.VERSION_CODES.N) +@Immutable +class HeartBeatSensorState internal constructor( + val isConfidentPeak: Boolean = false, + val isAvailable: Boolean = false, + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is HeartBeatSensorState) return false + + if (isConfidentPeak != other.isConfidentPeak) return false + if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false + + return true + } + + override fun hashCode(): Int { + var result = isConfidentPeak.hashCode() + result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() + return result + } + + override fun toString(): String { + return "HeartBeatSensorState(isConfidentPeak=$isConfidentPeak isAvailable=$isAvailable, " + + "accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } +} + +/** + * Creates and [remember]s an instance of [HeartBeatSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. + * @param sensorDelay The rate at which the raw sensor data should be received. + * Defaults to [SensorDelay.Normal]. + * @param onError Callback invoked on every error state. + */ +@RequiresApi(Build.VERSION_CODES.N) +@Composable +fun rememberHeartBeatSensorState( + autoStart: Boolean = true, + sensorDelay: SensorDelay = SensorDelay.Normal, + onError: (throwable: Throwable) -> Unit = {} +): HeartBeatSensorState { + val sensorState = rememberSensorState( + sensorType = SensorType.HeartBeat, + sensorDelay = sensorDelay, + autoStart = autoStart, + onError = onError + ) + val confidenceSensorState = remember { mutableStateOf(HeartBeatSensorState()) } + + LaunchedEffect( + key1 = sensorState, + block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + confidenceSensorState.value = HeartBeatSensorState( + isConfidentPeak = sensorStateValues[0] == 1f, + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening + ) + } + } + ) + + return confidenceSensorState.value +} diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/HeartRateSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/HeartRateSensorState.kt index 5f8f80d..572c811 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/HeartRateSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/HeartRateSensorState.kt @@ -17,7 +17,9 @@ class HeartRateSensorState internal constructor( val heartRate: Float = 0f, val isAvailable: Boolean = false, val accuracy: Int = 0, -) { + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is HeartRateSensorState) return false @@ -25,6 +27,8 @@ class HeartRateSensorState internal constructor( if (heartRate != other.heartRate) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -33,29 +37,44 @@ class HeartRateSensorState internal constructor( var result = heartRate.hashCode() result = 31 * result + isAvailable.hashCode() result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } override fun toString(): String { - return "HeartRateSensorState(heartRate=$heartRate isAvailable=$isAvailable, accuracy=$accuracy)" + return "HeartRateSensorState(heartRate=$heartRate isAvailable=$isAvailable," + + " accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() } } /** * Creates and [remember]s an instance of [HeartRateSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberHeartRateSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, - onError: (throwable: Throwable) -> Unit = {}, + onError: (throwable: Throwable) -> Unit = {} ): HeartRateSensorState { val sensorState = rememberSensorState( sensorType = SensorType.HeartRate, sensorDelay = sensorDelay, - onError = onError, + autoStart = autoStart, + onError = onError ) val heartRateSensorState = remember { mutableStateOf(HeartRateSensorState()) } @@ -68,9 +87,11 @@ fun rememberHeartRateSensorState( heartRate = sensorStateValues[0], isAvailable = sensorState.isAvailable, accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } - }, + } ) return heartRateSensorState.value diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/HingeAngleSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/HingeAngleSensorState.kt index b3c2882..503b344 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/HingeAngleSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/HingeAngleSensorState.kt @@ -21,8 +21,10 @@ import androidx.compose.runtime.remember class HingeAngleSensorState internal constructor( val angle: Float = 0f, val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is HingeAngleSensorState) return false @@ -30,6 +32,8 @@ class HingeAngleSensorState internal constructor( if (angle != other.angle) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -38,16 +42,28 @@ class HingeAngleSensorState internal constructor( var result = angle.hashCode() result = 31 * result + isAvailable.hashCode() result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } override fun toString(): String { return "HingeAngleSensorState(angle=$angle, isAvailable=$isAvailable, accuracy=$accuracy)" } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } } /** * Creates and [remember]s an instance of [HingeAngleSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. @@ -55,12 +71,14 @@ class HingeAngleSensorState internal constructor( @RequiresApi(Build.VERSION_CODES.R) @Composable fun rememberHingeAngleSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, onError: (throwable: Throwable) -> Unit = {} ): HingeAngleSensorState { val sensorState = rememberSensorState( sensorType = SensorType.HingeAngle, sensorDelay = sensorDelay, + autoStart = autoStart, onError = onError ) val hingeAngleSensorState = remember { mutableStateOf(HingeAngleSensorState()) } @@ -73,7 +91,9 @@ fun rememberHingeAngleSensorState( hingeAngleSensorState.value = HingeAngleSensorState( angle = sensorStateValues[0], isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/LightSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/LightSensorState.kt index 04343da..f44718e 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/LightSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/LightSensorState.kt @@ -17,7 +17,9 @@ class LightSensorState internal constructor( val illuminance: Float = 0f, val isAvailable: Boolean = false, val accuracy: Int = 0, -) { + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is LightSensorState) return false @@ -25,6 +27,8 @@ class LightSensorState internal constructor( if (illuminance != other.illuminance) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -33,30 +37,44 @@ class LightSensorState internal constructor( var result = illuminance.hashCode() result = 31 * result + isAvailable.hashCode() result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } override fun toString(): String { return "LightSensorState(illuminance=$illuminance, isAvailable=$isAvailable, " + - "accuracy=$accuracy)" + "accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() } } /** * Creates and [remember]s an instance of [LightSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberLightSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, - onError: (throwable: Throwable) -> Unit = {}, + onError: (throwable: Throwable) -> Unit = {} ): LightSensorState { val sensorState = rememberSensorState( sensorType = SensorType.Light, sensorDelay = sensorDelay, - onError = onError, + autoStart = autoStart, + onError = onError ) val lightSensorState = remember { mutableStateOf(LightSensorState()) } @@ -68,7 +86,9 @@ fun rememberLightSensorState( lightSensorState.value = LightSensorState( illuminance = sensorStateValues[0], isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/LimitedAxesAccelerometerSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/LimitedAxesAccelerometerSensorState.kt index 0909840..088d371 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/LimitedAxesAccelerometerSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/LimitedAxesAccelerometerSensorState.kt @@ -27,8 +27,10 @@ class LimitedAxesAccelerometerSensorState internal constructor( val yAxisSupported: Boolean = false, val zAxisSupported: Boolean = false, val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -42,6 +44,8 @@ class LimitedAxesAccelerometerSensorState internal constructor( if (zAxisSupported != other.zAxisSupported) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -54,7 +58,9 @@ class LimitedAxesAccelerometerSensorState internal constructor( result = 31 * result + yAxisSupported.hashCode() result = 31 * result + zAxisSupported.hashCode() result = 31 * result + isAvailable.hashCode() - result = 31 * result + accuracy + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } @@ -63,25 +69,38 @@ class LimitedAxesAccelerometerSensorState internal constructor( "zForce=$zForce, xAxisSupported=$xAxisSupported, yAxisSupported=$yAxisSupported, " + "zAxisSupported=$zAxisSupported, isAvailable=$isAvailable, accuracy=$accuracy)" } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } } /** * Creates and [remember]s an instance of [LimitedAxesAccelerometerSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberLimitedAxesAccelerometerSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, onError: (throwable: Throwable) -> Unit = {} ): LimitedAxesAccelerometerSensorState { val sensorState = rememberSensorState( sensorType = SensorType.AccelerometerLimitedAxes, sensorDelay = sensorDelay, + autoStart = autoStart, onError = onError ) - val accelerometerSensorState = remember { mutableStateOf(LimitedAxesAccelerometerSensorState()) } + val accelerometerSensorState = + remember { mutableStateOf(LimitedAxesAccelerometerSensorState()) } LaunchedEffect( key1 = sensorState, @@ -96,7 +115,9 @@ fun rememberLimitedAxesAccelerometerSensorState( yAxisSupported = sensorStateValues[4] != 0f, zAxisSupported = sensorStateValues[5] != 0f, isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/LimitedAxesGyroscopeSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/LimitedAxesGyroscopeSensorState.kt index ed9a334..42110dd 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/LimitedAxesGyroscopeSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/LimitedAxesGyroscopeSensorState.kt @@ -28,8 +28,10 @@ class LimitedAxesGyroscopeSensorState internal constructor( val yAxisSupported: Boolean = false, val zAxisSupported: Boolean = false, val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -45,6 +47,8 @@ class LimitedAxesGyroscopeSensorState internal constructor( if (zAxisSupported != other.zAxisSupported) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -57,7 +61,9 @@ class LimitedAxesGyroscopeSensorState internal constructor( result = 31 * result + yAxisSupported.hashCode() result = 31 * result + zAxisSupported.hashCode() result = 31 * result + isAvailable.hashCode() - result = 31 * result + accuracy + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } @@ -67,10 +73,20 @@ class LimitedAxesGyroscopeSensorState internal constructor( "yAxisSupported=$yAxisSupported, zAxisSupported=$zAxisSupported, " + "isAvailable=$isAvailable, accuracy=$accuracy)" } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } } /** * Creates and [remember]s an instance of [LimitedAxesGyroscopeSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. @@ -78,12 +94,14 @@ class LimitedAxesGyroscopeSensorState internal constructor( @RequiresApi(Build.VERSION_CODES.TIRAMISU) @Composable fun rememberLimitedAxesGyroscopeSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, onError: (throwable: Throwable) -> Unit = {} ): LimitedAxesGyroscopeSensorState { val sensorState = rememberSensorState( sensorType = SensorType.GyroscopeLimitedAxes, sensorDelay = sensorDelay, + autoStart = autoStart, onError = onError ) @@ -100,7 +118,9 @@ fun rememberLimitedAxesGyroscopeSensorState( yAxisSupported = sensorStateValues[4] != 0f, zAxisSupported = sensorStateValues[5] != 0f, isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } }) diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/LinearAccelerationSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/LinearAccelerationSensorState.kt index dfb90fb..7f10cff 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/LinearAccelerationSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/LinearAccelerationSensorState.kt @@ -23,8 +23,10 @@ class LinearAccelerationSensorState internal constructor( val yForce: Float = 0f, val zForce: Float = 0f, val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is LinearAccelerationSensorState) return false @@ -34,6 +36,8 @@ class LinearAccelerationSensorState internal constructor( if (zForce != other.zForce) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -44,6 +48,8 @@ class LinearAccelerationSensorState internal constructor( result = 31 * result + zForce.hashCode() result = 31 * result + isAvailable.hashCode() result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } @@ -51,10 +57,20 @@ class LinearAccelerationSensorState internal constructor( return "LinearAccelerationSensorState(xForce=$xForce, yForce=$yForce, zForce=$zForce, " + "isAvailable=$isAvailable, accuracy=$accuracy)" } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } } /** * Creates and [remember]s an instance of [LinearAccelerationSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. @@ -62,12 +78,14 @@ class LinearAccelerationSensorState internal constructor( @RequiresApi(Build.VERSION_CODES.TIRAMISU) @Composable fun rememberLinearAccelerationSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, onError: (throwable: Throwable) -> Unit = {} ): LinearAccelerationSensorState { val sensorState = rememberSensorState( sensorType = SensorType.LinearAcceleration, sensorDelay = sensorDelay, + autoStart = autoStart, onError = onError ) val linearAccelerationSensorState = remember { mutableStateOf(LinearAccelerationSensorState()) } @@ -82,7 +100,9 @@ fun rememberLinearAccelerationSensorState( yForce = sensorStateValues[1], zForce = sensorStateValues[2], isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/LowLatencyOffBodyDetectSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/LowLatencyOffBodyDetectSensorState.kt index bd05424..d8af958 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/LowLatencyOffBodyDetectSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/LowLatencyOffBodyDetectSensorState.kt @@ -20,8 +20,10 @@ import androidx.compose.runtime.remember class LowLatencyOffBodyDetectSensorState internal constructor( val isDeviceOnBody: Boolean = false, val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is LowLatencyOffBodyDetectSensorState) return false @@ -29,6 +31,8 @@ class LowLatencyOffBodyDetectSensorState internal constructor( if (isDeviceOnBody != other.isDeviceOnBody) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -37,16 +41,29 @@ class LowLatencyOffBodyDetectSensorState internal constructor( var result = isDeviceOnBody.hashCode() result = 31 * result + isAvailable.hashCode() result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } override fun toString(): String { - return "LowLatencyOffBodyDetectSensorState(isDeviceOnBody=$isDeviceOnBody, isAvailable=$isAvailable, accuracy=$accuracy)" + return "LowLatencyOffBodyDetectSensorState(isDeviceOnBody=$isDeviceOnBody," + + " isAvailable=$isAvailable, accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() } } /** * Creates and [remember]s an instance of [LowLatencyOffBodyDetectSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. @@ -54,12 +71,14 @@ class LowLatencyOffBodyDetectSensorState internal constructor( @RequiresApi(Build.VERSION_CODES.O) @Composable fun rememberLowLatencyOffBodyDetectSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, onError: (throwable: Throwable) -> Unit = {} ): LowLatencyOffBodyDetectSensorState { val sensorState = rememberSensorState( sensorType = SensorType.LowLatencyOffBodyDetect, sensorDelay = sensorDelay, + autoStart = autoStart, onError = onError ) val lowLatencyOffBodyDetectSensorState = @@ -73,7 +92,9 @@ fun rememberLowLatencyOffBodyDetectSensorState( lowLatencyOffBodyDetectSensorState.value = LowLatencyOffBodyDetectSensorState( isDeviceOnBody = sensorStateValues[0].toInt() == 1, isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/MagneticFieldSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/MagneticFieldSensorState.kt index 722e1e0..69eeb76 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/MagneticFieldSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/MagneticFieldSensorState.kt @@ -21,7 +21,9 @@ class MagneticFieldSensorState internal constructor( val zStrength: Float = 0f, val isAvailable: Boolean = false, val accuracy: Int = 0, -) { + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is MagneticFieldSensorState) return false @@ -31,6 +33,8 @@ class MagneticFieldSensorState internal constructor( if (zStrength != other.zStrength) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -41,30 +45,44 @@ class MagneticFieldSensorState internal constructor( result = 31 * result + zStrength.hashCode() result = 31 * result + isAvailable.hashCode() result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } override fun toString(): String { return "MagneticFieldSensorState(xStrength=$xStrength, yStrength=$yStrength, " + - "zStrength=$zStrength, isAvailable=$isAvailable, accuracy=$accuracy)" + "zStrength=$zStrength, isAvailable=$isAvailable, accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() } } /** * Creates and [remember]s an instance of [MagneticFieldSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberMagneticFieldSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, - onError: (throwable: Throwable) -> Unit = {}, + onError: (throwable: Throwable) -> Unit = {} ): MagneticFieldSensorState { val sensorState = rememberSensorState( sensorType = SensorType.MagneticField, sensorDelay = sensorDelay, - onError = onError, + autoStart = autoStart, + onError = onError ) val magneticFieldSensorState = remember { mutableStateOf(MagneticFieldSensorState()) } @@ -78,7 +96,9 @@ fun rememberMagneticFieldSensorState( yStrength = sensorStateValues[1], zStrength = sensorStateValues[2], isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/MotionDetectSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/MotionDetectSensorState.kt new file mode 100644 index 0000000..15724c4 --- /dev/null +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/MotionDetectSensorState.kt @@ -0,0 +1,110 @@ +package com.mutualmobile.composesensors + +import android.hardware.Sensor +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember + +/** + * The Stationary Detect sensor ([Sensor.TYPE_MOTION_DETECT]) is used to detect whether the + * device is stationary or in motion. This sensor produces an event if the device has been in motion + * for at least 5 seconds with a maximal latency of 5 additional seconds. ie: it may take up + * anywhere from 5 to 10 seconds after the device has been at rest to trigger this event. The only + * allowed value is 1.0 (true). + * + * For more info, please refer the [Android Documentation Reference](https://developer.android.com/reference/android/hardware/SensorEvent#sensor.type_motion_detect:) + * + * @param isDeviceInMotion Whether the device is in motion or not. Defaults to false. + * @param isAvailable Whether the current device has a heart beat sensor. Defaults to false. + * @param accuracy Accuracy factor of the heart beat sensor. Defaults to 0. + */ +@RequiresApi(Build.VERSION_CODES.N) +@Immutable +class MotionDetectSensorState internal constructor( + val isDeviceInMotion: Boolean = false, + val isAvailable: Boolean = false, + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is MotionDetectSensorState) return false + + if (isDeviceInMotion != other.isDeviceInMotion) return false + if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false + + return true + } + + override fun hashCode(): Int { + var result = isDeviceInMotion.hashCode() + result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() + return result + } + + override fun toString(): String { + return "MotionDetectSensorState(isDeviceInMotion=$isDeviceInMotion," + + " isAvailable=$isAvailable, accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } +} + +/** + * Creates and [remember]s an instance of [MotionDetectSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. + * @param sensorDelay The rate at which the raw sensor data should be received. + * Defaults to [SensorDelay.Normal]. + * @param onError Callback invoked on every error state. + */ +@RequiresApi(Build.VERSION_CODES.N) +@Composable +fun rememberMotionDetectSensorState( + autoStart: Boolean = true, + sensorDelay: SensorDelay = SensorDelay.Normal, + onError: (throwable: Throwable) -> Unit = {} +): MotionDetectSensorState { + val sensorState = rememberSensorState( + sensorType = SensorType.MotionDetect, + sensorDelay = sensorDelay, + autoStart = autoStart, + onError = onError + ) + val confidenceSensorState = remember { mutableStateOf(MotionDetectSensorState()) } + + LaunchedEffect( + key1 = sensorState, + block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + confidenceSensorState.value = MotionDetectSensorState( + isDeviceInMotion = sensorStateValues[0] == 1f, + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening + ) + } + } + ) + + return confidenceSensorState.value +} diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/PressureSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/PressureSensorState.kt index 598cfb2..b601193 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/PressureSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/PressureSensorState.kt @@ -16,8 +16,10 @@ import androidx.compose.runtime.remember class PressureSensorState internal constructor( val pressure: Float = 0f, val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is PressureSensorState) return false @@ -25,6 +27,8 @@ class PressureSensorState internal constructor( if (pressure != other.pressure) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -33,28 +37,43 @@ class PressureSensorState internal constructor( var result = pressure.hashCode() result = 31 * result + isAvailable.hashCode() result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } override fun toString(): String { - return "PressureSensorState(pressure=$pressure, isAvailable=$isAvailable, accuracy=$accuracy)" + return "PressureSensorState(pressure=$pressure, isAvailable=$isAvailable," + + " accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() } } /** * Creates and [remember]s an instance of [PressureSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberPressureSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, onError: (throwable: Throwable) -> Unit = {} ): PressureSensorState { val sensorState = rememberSensorState( sensorType = SensorType.Pressure, sensorDelay = sensorDelay, + autoStart = autoStart, onError = onError ) val pressureSensorState = remember { mutableStateOf(PressureSensorState()) } @@ -67,7 +86,9 @@ fun rememberPressureSensorState( pressureSensorState.value = PressureSensorState( pressure = sensorStateValues[0], isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/ProximitySensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/ProximitySensorState.kt index 40224db..1bc319c 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/ProximitySensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/ProximitySensorState.kt @@ -14,63 +14,82 @@ import androidx.compose.runtime.remember */ @Immutable class ProximitySensorState internal constructor( - val sensorDistance: Float = 0f, - val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { - override fun toString(): String { - return "ProximitySensorState(sensorDistance=$sensorDistance, " + - "isAvailable=$isAvailable, accuracy=$accuracy)" - } + val sensorDistance: Float = 0f, + val isAvailable: Boolean = false, + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ProximitySensorState) return false - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is ProximitySensorState) return false + if (sensorDistance != other.sensorDistance) return false + if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false + return true + } - if (isAvailable != other.isAvailable) return false - if (sensorDistance != other.sensorDistance) return false - if (accuracy != other.accuracy) return false - return true - } + override fun hashCode(): Int { + var result = isAvailable.hashCode() + result = 31 * result + sensorDistance.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() + return result + } - override fun hashCode(): Int { - var result = isAvailable.hashCode() - result = 31 * result + sensorDistance.hashCode() - result = 31 * result + accuracy.hashCode() - return result - } -} + override fun toString(): String { + return "ProximitySensorState(sensorDistance=$sensorDistance, " + + "isAvailable=$isAvailable, accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + override fun stopListening() { + stopListeningEvents?.invoke() + } +} /** * Creates and [remember]s instance of [ProximitySensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberProximitySensorState( - sensorDelay: SensorDelay = SensorDelay.Normal, - onError: (throwable: Throwable) -> Unit = {}, + autoStart: Boolean = true, + sensorDelay: SensorDelay = SensorDelay.Normal, + onError: (throwable: Throwable) -> Unit = {} ): ProximitySensorState { - val sensorState = rememberSensorState( - sensorType = SensorType.Proximity, - sensorDelay = sensorDelay, - onError = onError, - ) + val sensorState = rememberSensorState( + sensorType = SensorType.Proximity, + sensorDelay = sensorDelay, + autoStart = autoStart, + onError = onError + ) - val proximitySensorState = remember { mutableStateOf(ProximitySensorState()) } + val proximitySensorState = remember { mutableStateOf(ProximitySensorState()) } - LaunchedEffect(key1 = sensorState, block = { - val sensorStateValues = sensorState.data - if (sensorStateValues.isNotEmpty()) { - proximitySensorState.value = ProximitySensorState( - sensorDistance = sensorStateValues[0], - isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy - ) - } - }) + LaunchedEffect(key1 = sensorState, block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + proximitySensorState.value = ProximitySensorState( + sensorDistance = sensorStateValues[0], + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening + ) + } + }) - return proximitySensorState.value -} \ No newline at end of file + return proximitySensorState.value +} diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/RelativeHumiditySensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/RelativeHumiditySensorState.kt index 4eb2631..cff89a5 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/RelativeHumiditySensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/RelativeHumiditySensorState.kt @@ -25,15 +25,21 @@ class RelativeHumiditySensorState internal constructor( val relativeHumidity: Float = 0f, val isAvailable: Boolean = false, val actualTemp: Float = 0f, - val accuracy: Int = 0 -) { - val absoluteHumidity = 216.7 * - ( - relativeHumidity / 100.0 * 6.112 * exp( - 17.62 * actualTemp / - (243.12 + actualTemp) - ) / (273.15 + actualTemp) - ) + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { + val absoluteHumidity = 216.7 + .times(relativeHumidity) + .div(100.0) + .times(6.112) + .times( + exp( + 17.62 + .times(actualTemp) + .div(243.12.plus(actualTemp)) + ).div(273.15.plus(actualTemp)) + ) private val humidity = ln(relativeHumidity / 100.0) + (17.62 * actualTemp) / (243.12 + actualTemp) @@ -47,6 +53,8 @@ class RelativeHumiditySensorState internal constructor( if (relativeHumidity != other.relativeHumidity) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -55,6 +63,8 @@ class RelativeHumiditySensorState internal constructor( var result = relativeHumidity.hashCode() result = 31 * result + isAvailable.hashCode() result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } @@ -64,42 +74,57 @@ class RelativeHumiditySensorState internal constructor( "accuracy=$accuracy)" } - /** - * Creates and [remember]s an instance of [RelativeHumiditySensorState]. - * @param sensorDelay The rate at which the raw sensor data should be received. - * Defaults to [SensorDelay.Normal]. - * @param actualTemp is passed into the function only if User requires calculations on Dew Point and absolute Humidity - * This can be accessed using the dot operator. (e.g -> rememberRelativeHumiditySensorState().absoluteHumidity) - * @param onError Callback invoked on every error state. - */ - @Composable - fun rememberRelativeHumiditySensorState( - sensorDelay: SensorDelay = SensorDelay.Normal, - actualTemp: Float = 0F, - onError: (throwable: Throwable) -> Unit = {} - ): RelativeHumiditySensorState { - val sensorState = rememberSensorState( - sensorType = SensorType.RelativeHumidity, - sensorDelay = sensorDelay, - onError = onError - ) - val relativeHumiditySensorState = remember { mutableStateOf(RelativeHumiditySensorState()) } + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } +} - LaunchedEffect( - key1 = sensorState, - block = { - val sensorStateValues = sensorState.data - if (sensorStateValues.isNotEmpty()) { - relativeHumiditySensorState.value = RelativeHumiditySensorState( - relativeHumidity = sensorStateValues[0], - isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy, - actualTemp = actualTemp - ) - } +/** + * Creates and [remember]s an instance of [RelativeHumiditySensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. + * @param sensorDelay The rate at which the raw sensor data should be received. + * Defaults to [SensorDelay.Normal]. + * @param actualTemp is passed into the function only if User requires calculations on Dew Point + * and absolute Humidity. This can be accessed using the dot operator. (e.g -> + * rememberRelativeHumiditySensorState().absoluteHumidity) + * @param onError Callback invoked on every error state. + */ +@Composable +fun rememberRelativeHumiditySensorState( + autoStart: Boolean = true, + sensorDelay: SensorDelay = SensorDelay.Normal, + actualTemp: Float = 0f, + onError: (throwable: Throwable) -> Unit = {} +): RelativeHumiditySensorState { + val sensorState = rememberSensorState( + sensorType = SensorType.RelativeHumidity, + sensorDelay = sensorDelay, + autoStart = autoStart, + onError = onError + ) + val relativeHumiditySensorState = remember { mutableStateOf(RelativeHumiditySensorState()) } + + LaunchedEffect( + key1 = sensorState, + block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + relativeHumiditySensorState.value = RelativeHumiditySensorState( + relativeHumidity = sensorStateValues[0], + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy, + actualTemp = actualTemp, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening + ) } - ) + } + ) - return relativeHumiditySensorState.value - } + return relativeHumiditySensorState.value } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/RotationVectorSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/RotationVectorSensorState.kt index 30723fe..8bc8bda 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/RotationVectorSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/RotationVectorSensorState.kt @@ -18,80 +18,100 @@ import androidx.compose.runtime.remember */ @Immutable class RotationVectorSensorState internal constructor( - val vectorX: Float = 0f, - val vectorY: Float = 0f, - val vectorZ: Float = 0f, - val scalar: Float = 0f, - val estimatedHeadingAccuracy: Float = 0f, - val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { - override fun toString(): String { - return "RotationVectorSensorState(vectorX=$vectorX, " + - "vectorY=$vectorY, vectorZ=$vectorZ, scalar=$scalar, " + - "estimatedHeadingAccuracy=$estimatedHeadingAccuracy, " + - "isAvailable=$isAvailable, accuracy=$accuracy)" - } + val vectorX: Float = 0f, + val vectorY: Float = 0f, + val vectorZ: Float = 0f, + val scalar: Float = 0f, + val estimatedHeadingAccuracy: Float = 0f, + val isAvailable: Boolean = false, + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { + override fun equals(other: Any?): Boolean { + if (this === other) return true + other as RotationVectorSensorState - override fun equals(other: Any?): Boolean { - if (this === other) return true - other as RotationVectorSensorState + if (vectorX != other.vectorX) return false + if (vectorY != other.vectorY) return false + if (vectorZ != other.vectorZ) return false + if (scalar != other.scalar) return false + if (estimatedHeadingAccuracy != other.estimatedHeadingAccuracy) return false + if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false + return true + } - if (vectorX != other.vectorX) return false - if (vectorY != other.vectorY) return false - if (vectorZ != other.vectorZ) return false - if (scalar != other.scalar) return false - if (estimatedHeadingAccuracy != other.estimatedHeadingAccuracy) return false - if (isAvailable != other.isAvailable) return false - if (accuracy != other.accuracy) return false - return true - } + override fun hashCode(): Int { + var result = vectorX.hashCode() + result = 31 * result + vectorY.hashCode() + result = 31 * result + vectorZ.hashCode() + result = 31 * result + scalar.hashCode() + result = 31 * result + estimatedHeadingAccuracy.hashCode() + result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() + return result + } - override fun hashCode(): Int { - var result = vectorX.hashCode() - result = 31 * result + vectorY.hashCode() - result = 31 * result + vectorZ.hashCode() - result = 31 * result + scalar.hashCode() - result = 31 * result + estimatedHeadingAccuracy.hashCode() - result = 31 * result + isAvailable.hashCode() - result = 31 * result + accuracy - return result - } + override fun toString(): String { + return "RotationVectorSensorState(vectorX=$vectorX, " + + "vectorY=$vectorY, vectorZ=$vectorZ, scalar=$scalar, " + + "estimatedHeadingAccuracy=$estimatedHeadingAccuracy, " + + "isAvailable=$isAvailable, accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } } /** * Creates and [remember]s instance of [RotationVectorSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberRotationVectorSensorState( - sensorDelay: SensorDelay = SensorDelay.Normal, - onError: (throwable: Throwable) -> Unit = {}, + autoStart: Boolean = true, + sensorDelay: SensorDelay = SensorDelay.Normal, + onError: (throwable: Throwable) -> Unit = {} ): RotationVectorSensorState { - val sensorState = rememberSensorState( - sensorType = SensorType.RotationVector, - sensorDelay = sensorDelay, - onError = onError, - ) + val sensorState = rememberSensorState( + sensorType = SensorType.RotationVector, + sensorDelay = sensorDelay, + autoStart = autoStart, + onError = onError + ) - val rotationVectorSensorState = remember { mutableStateOf(RotationVectorSensorState()) } + val rotationVectorSensorState = remember { mutableStateOf(RotationVectorSensorState()) } - LaunchedEffect(key1 = sensorState, block = { - val sensorStateValues = sensorState.data - if (sensorStateValues.isNotEmpty()) { - rotationVectorSensorState.value = RotationVectorSensorState( - vectorX = sensorStateValues[0], - vectorY = sensorStateValues[1], - vectorZ = sensorStateValues[2], - scalar = sensorStateValues[3], - estimatedHeadingAccuracy = sensorStateValues[4], - isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy - ) - } - }) + LaunchedEffect(key1 = sensorState, block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + rotationVectorSensorState.value = RotationVectorSensorState( + vectorX = sensorStateValues[0], + vectorY = sensorStateValues[1], + vectorZ = sensorStateValues[2], + scalar = sensorStateValues[3], + estimatedHeadingAccuracy = sensorStateValues[4], + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening + ) + } + }) - return rotationVectorSensorState.value -} \ No newline at end of file + return rotationVectorSensorState.value +} diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/SensorDelay.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/SensorDelay.kt index ad21646..ccf2490 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/SensorDelay.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/SensorDelay.kt @@ -5,6 +5,8 @@ import android.hardware.SensorManager enum class SensorDelay { /** * Get sensor data as fast as possible + * - **``** may + * be required to be added in AndroidManifest.xml for this option to work */ Fastest, diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/SensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/SensorState.kt index ee9139e..468c394 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/SensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/SensorState.kt @@ -5,6 +5,8 @@ import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager +import android.hardware.TriggerEvent +import android.hardware.TriggerEventListener import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Immutable @@ -21,32 +23,38 @@ import androidx.compose.ui.platform.LocalContext * positioning, or you want to monitor changes in the ambient environment near a device. * * SensorState provides the availability status and the current data about a sensor. + * @param data Data from the sensor. Defaults to an empty list. * @param isAvailable Whether the required sensor is available in the current device. Defaults to * false. - * @param data Data from the sensor. Defaults to an empty list. * @param accuracy Accuracy factor of the current sensor. Defaults to 0. */ @Immutable internal class SensorState( - val isAvailable: Boolean = false, val data: List = emptyList(), + val isAvailable: Boolean = false, val accuracy: Int = 0, -) { + private val triggerEvent: () -> Unit, + private val isListenerStarted: MutableState +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is SensorState) return false - if (isAvailable != other.isAvailable) return false if (data != other.data) return false + if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (triggerEvent != other.triggerEvent) return false + if (isListenerStarted != other.isListenerStarted) return false return true } override fun hashCode(): Int { - var result = isAvailable.hashCode() - result = 31 * result + data.hashCode() + var result = data.hashCode() + result = 31 * result + isAvailable.hashCode() result = 31 * result + accuracy.hashCode() + result = 31 * result + triggerEvent.hashCode() + result = 31 * result + isListenerStarted.hashCode() return result } @@ -54,67 +62,117 @@ internal class SensorState( return "SensorState(isAvailable=$isAvailable, data=${data.joinToString()}, " + "accuracy=$accuracy)" } + + /** + * Use this function to trigger the event request for sensors that emit One-Shot events (For + * example: [SignificantMotionSensorState]). + */ + internal fun requestEventTrigger() = triggerEvent() + + override fun startListening() { + isListenerStarted.value = true + } + + override fun stopListening() { + isListenerStarted.value = false + } } @Composable internal fun rememberSensorState( sensorType: SensorType, - sensorDelay: SensorDelay, + sensorDelay: SensorDelay? = null, + autoStart: Boolean = true, onError: (throwable: Throwable) -> Unit, + onMotionEvent: (Long) -> Unit = {} ): SensorState { + val isListenerStarted = remember { mutableStateOf(autoStart) } val isSensorAvailable = sensorType.rememberIsSensorAvailable() val sensorData: MutableState> = remember { mutableStateOf(emptyList()) } val sensorAccuracy: MutableState = remember { mutableStateOf(0) } + val sensorManager = + LocalContext.current.getSystemService(Context.SENSOR_SERVICE) as SensorManager val sensorState = remember { derivedStateOf { SensorState( isAvailable = isSensorAvailable, data = sensorData.value, accuracy = sensorAccuracy.value, + triggerEvent = { + runCatching { + val sensor = + sensorManager.getDefaultSensor(sensorType.toAndroidSensorType()) + ?: throw SensorNotFoundException(sensorName = sensorType.name) + + if (sensorType == SensorType.SignificantMotion) { + val triggerEventListener = object : TriggerEventListener() { + override fun onTrigger(event: TriggerEvent?) { + onMotionEvent(event?.timestamp ?: 0L) + } + } + sensorManager.requestTriggerSensor(triggerEventListener, sensor) + } + }.onFailure(onError) + }, + isListenerStarted = isListenerStarted ) } } runCatching { - val sensorManager = - LocalContext.current.getSystemService(Context.SENSOR_SERVICE) as SensorManager - val sensor = sensorManager.getDefaultSensor(sensorType.toAndroidSensorType()) ?: throw SensorNotFoundException(sensorName = sensorType.name) DisposableEffect( key1 = sensor, key2 = sensorDelay, + key3 = isListenerStarted.value, effect = { - val sensorEventListener = object : SensorEventListener { - override fun onSensorChanged(event: SensorEvent?) { - event?.let { nnEvent -> - nnEvent.values?.let { nnValues -> - sensorData.value = nnValues.toList() + var sensorEventListener: SensorEventListener? = null + if (isListenerStarted.value) { + sensorEventListener = object : SensorEventListener { + override fun onSensorChanged(event: SensorEvent?) { + event?.let { nnEvent -> + nnEvent.values?.let { nnValues -> + sensorData.value = nnValues.toList() + } } } - } - override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { - sensorAccuracy.value = accuracy + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + sensorAccuracy.value = accuracy + } } - } - sensorManager.registerListener( - sensorEventListener, - sensor, - sensorDelay.toAndroidSensorDelay(), - ) + sensorManager.registerListener( + sensorEventListener, + sensor, + (sensorDelay ?: SensorDelay.Normal).toAndroidSensorDelay() + ) + } onDispose { sensorManager.unregisterListener(sensorEventListener) + isListenerStarted.value = false } - }, + } ) }.onFailure(onError) return sensorState.value } +interface SensorStateListener { + /** + * Start listening to this sensor's values + */ + fun startListening() + + /** + * Stop listening to this sensor's values + */ + fun stopListening() +} + private class SensorNotFoundException(sensorName: String) : Exception("The required sensor '$sensorName' was not found on the current device.") diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/SensorType.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/SensorType.kt index e9b17ce..e66cec6 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/SensorType.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/SensorType.kt @@ -43,7 +43,6 @@ sealed class SensorType(val name: String) { object GyroscopeLimitedAxesUncalibrated : SensorType(name = "GyroscopeLimitedAxesUncalibrated") object Heading : SensorType(name = "Heading") - object All : SensorType(name = "All") fun toAndroidSensorType(): Int { return when (this) { @@ -73,25 +72,30 @@ sealed class SensorType(val name: String) { is LowLatencyOffBodyDetect -> checkApi(Build.VERSION_CODES.O) { Sensor.TYPE_LOW_LATENCY_OFFBODY_DETECT } + is AccelerometerUncalibrated -> checkApi(Build.VERSION_CODES.O) { Sensor.TYPE_ACCELEROMETER_UNCALIBRATED } + is HingeAngle -> checkApi(Build.VERSION_CODES.R) { Sensor.TYPE_HINGE_ANGLE } is HeadTracker -> checkApi(Build.VERSION_CODES.TIRAMISU) { Sensor.TYPE_HEAD_TRACKER } is AccelerometerLimitedAxes -> checkApi(Build.VERSION_CODES.TIRAMISU) { Sensor.TYPE_ACCELEROMETER_LIMITED_AXES } + is GyroscopeLimitedAxes -> checkApi(Build.VERSION_CODES.TIRAMISU) { Sensor.TYPE_GYROSCOPE_LIMITED_AXES } + is AccelerometerLimitedAxesUncalibrated -> checkApi(Build.VERSION_CODES.TIRAMISU) { Sensor.TYPE_ACCELEROMETER_LIMITED_AXES_UNCALIBRATED } + is GyroscopeLimitedAxesUncalibrated -> checkApi(Build.VERSION_CODES.TIRAMISU) { Sensor.TYPE_GYROSCOPE_LIMITED_AXES_UNCALIBRATED } + is Heading -> checkApi(Build.VERSION_CODES.TIRAMISU) { Sensor.TYPE_HEADING } - is All -> Sensor.TYPE_ALL } } @@ -106,14 +110,18 @@ sealed class SensorType(val name: String) { } private inline fun checkApi(expectedApi: Int, block: () -> T): T { - return if (Build.VERSION.SDK_INT < expectedApi) throw OldApiException( - currentApi = Build.VERSION.SDK_INT, - expectedApi = expectedApi - ) else block() + return if (Build.VERSION.SDK_INT < expectedApi) { + throw OldApiException( + currentApi = Build.VERSION.SDK_INT, + expectedApi = expectedApi + ) + } else { + block() + } } private class OldApiException(currentApi: Int, expectedApi: Int) : Exception( "The current API ($currentApi) is too low. At least API ($expectedApi) is required to use" + - "this sensor." + "this sensor." ) diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/SignificantMotionSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/SignificantMotionSensorState.kt new file mode 100644 index 0000000..d521607 --- /dev/null +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/SignificantMotionSensorState.kt @@ -0,0 +1,130 @@ +package com.mutualmobile.composesensors + +import android.hardware.Sensor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember + +/** + * A significant motion ([Sensor.TYPE_SIGNIFICANT_MOTION]) detector triggers when detecting a + * significant motion: a motion that might lead to a change in the user location. + * + * Examples of such significant motions are: + * - Walking or biking + * - Sitting in a moving car, coach, or train + * + * Examples of situations that don't trigger significant motion: + * - Phone in pocket and person isn't moving + * - Phone is on a table and the table shakes a bit due to nearby traffic or washing machine + * + * For more info, please refer the [Android Documentation Reference](https://source.android.com/docs/core/interaction/sensors/sensor-types#significant_motion) + * + * @param isListening Whether the app is currently listening to the motion event. Defaults to false. + * @param lastEventTimestamp Timestamp (in nanoseconds) of the last motion event. Defaults to 0L. + * @param isAvailable Whether the current device has a heart beat sensor. Defaults to false. + * @param accuracy Accuracy factor of the heart beat sensor. Defaults to 0. + */ +@Immutable +class SignificantMotionSensorState internal constructor( + private val triggerEvent: () -> Unit = {}, + val isListening: Boolean = false, + val lastEventTimestamp: Long = 0L, + val isAvailable: Boolean = false, + val accuracy: Int = 0, + private val startListeningEvents: () -> Unit, + private val stopListeningEvents: () -> Unit +) : SensorStateListener { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is SignificantMotionSensorState) return false + + if (isListening != other.isListening) return false + if (lastEventTimestamp != other.lastEventTimestamp) return false + if (triggerEvent != other.triggerEvent) return false + if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false + + return true + } + + override fun hashCode(): Int { + var result = triggerEvent.hashCode() + result = 31 * result + isListening.hashCode() + result = 31 * result + lastEventTimestamp.hashCode() + result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() + return result + } + + override fun toString(): String { + return "SignificantMotionSensorState(triggerEvent=$triggerEvent, " + + "isListening=$isListening, lastEventTimestamp=$lastEventTimestamp, " + + "isAvailable=$isAvailable, accuracy=$accuracy)" + } + + /** + * Since this sensor's reporting mode is One-Shot, therefore the user has to manually request a + * one-time event trigger and subscribe to it. Use this function to trigger such event and use + * onMotionEvent to subscribe to the event. + */ + fun requestEventTrigger() = triggerEvent() + + override fun startListening() = startListeningEvents() + + override fun stopListening() = stopListeningEvents() +} + +/** + * Creates and [remember]s an instance of [SignificantMotionSensorState]. + * @param onMotionEvent Callback invoked every time the sensor detects a significant amount of + * device motion. Since this sensor's reporting mode is One-Shot, use + * [SignificantMotionSensorState.requestEventTrigger] to trigger the event emission and collect + * the value (timestamp in nanoseconds - defaults to 0L) of the event in this callback. + * @param onError Callback invoked on every error state. + */ +@Composable +fun rememberSignificantMotionSensorState( + onMotionEvent: (timestamp: Long) -> Unit, + onError: (throwable: Throwable) -> Unit = {} +): SignificantMotionSensorState { + val isListening = remember { mutableStateOf(false) } + val lastEventTimestamp = remember { mutableStateOf(0L) } + + val sensorState = rememberSensorState( + sensorType = SensorType.SignificantMotion, + onError = onError, + onMotionEvent = { timestampNs -> + onMotionEvent(timestampNs) + isListening.value = false + lastEventTimestamp.value = timestampNs + } + ) + + val significantMotionSensorState = remember( + sensorState, + isListening.value, + lastEventTimestamp.value + ) { + mutableStateOf( + SignificantMotionSensorState( + triggerEvent = { + sensorState.requestEventTrigger() + isListening.value = true + }, + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy, + isListening = isListening.value, + lastEventTimestamp = lastEventTimestamp.value, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening + ) + ) + } + + return significantMotionSensorState.value +} diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/StationaryDetectSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/StationaryDetectSensorState.kt new file mode 100644 index 0000000..496a003 --- /dev/null +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/StationaryDetectSensorState.kt @@ -0,0 +1,108 @@ +package com.mutualmobile.composesensors + +import android.hardware.Sensor +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember + +/** + * The Stationary Detect sensor ([Sensor.TYPE_STATIONARY_DETECT]) is used to detect whether the + * device is stationary or in motion. This sensor provides a binary output, with a value of either + * false or true, indicating the device's motion state. + * + * For more info, please refer the [Android Documentation Reference](https://developer.android.com/reference/android/hardware/SensorEvent#sensor.type_stationary_detect:) + * + * @param isDeviceStationary Whether the device is stationary or not. Defaults to false. + * @param isAvailable Whether the current device has a heart beat sensor. Defaults to false. + * @param accuracy Accuracy factor of the heart beat sensor. Defaults to 0. + */ +@RequiresApi(Build.VERSION_CODES.N) +@Immutable +class StationaryDetectSensorState internal constructor( + val isDeviceStationary: Boolean = false, + val isAvailable: Boolean = false, + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is StationaryDetectSensorState) return false + + if (isDeviceStationary != other.isDeviceStationary) return false + if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false + + return true + } + + override fun hashCode(): Int { + var result = isDeviceStationary.hashCode() + result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() + return result + } + + override fun toString(): String { + return "StationaryDetectSensorState(isDeviceStationary=$isDeviceStationary," + + " isAvailable=$isAvailable, accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } +} + +/** + * Creates and [remember]s an instance of [StationaryDetectSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. + * @param sensorDelay The rate at which the raw sensor data should be received. + * Defaults to [SensorDelay.Normal]. + * @param onError Callback invoked on every error state. + */ +@RequiresApi(Build.VERSION_CODES.N) +@Composable +fun rememberStationaryDetectSensorState( + autoStart: Boolean = true, + sensorDelay: SensorDelay = SensorDelay.Normal, + onError: (throwable: Throwable) -> Unit = {} +): StationaryDetectSensorState { + val sensorState = rememberSensorState( + sensorType = SensorType.StationaryDetect, + sensorDelay = sensorDelay, + autoStart = autoStart, + onError = onError + ) + val confidenceSensorState = remember { mutableStateOf(StationaryDetectSensorState()) } + + LaunchedEffect( + key1 = sensorState, + block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + confidenceSensorState.value = StationaryDetectSensorState( + isDeviceStationary = sensorStateValues[0] == 1f, + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening + ) + } + } + ) + + return confidenceSensorState.value +} diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/StepCounterSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/StepCounterSensorState.kt index bf7abf3..f299c9c 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/StepCounterSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/StepCounterSensorState.kt @@ -18,7 +18,9 @@ class StepCounterSensorState internal constructor( val stepCount: Float = 0f, val isAvailable: Boolean = false, val accuracy: Int = 0, -) { + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is StepCounterSensorState) return false @@ -26,6 +28,8 @@ class StepCounterSensorState internal constructor( if (stepCount != other.stepCount) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -34,29 +38,44 @@ class StepCounterSensorState internal constructor( var result = stepCount.hashCode() result = 31 * result + isAvailable.hashCode() result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } override fun toString(): String { - return "StepCounterSensorState(stepCount=$stepCount isAvailable=$isAvailable, accuracy=$accuracy)" + return "StepCounterSensorState(stepCount=$stepCount isAvailable=$isAvailable," + + " accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() } } /** * Creates and [remember]s an instance of [StepCounterSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberStepCounterSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, - onError: (throwable: Throwable) -> Unit = {}, + onError: (throwable: Throwable) -> Unit = {} ): StepCounterSensorState { val sensorState = rememberSensorState( sensorType = SensorType.StepCounter, sensorDelay = sensorDelay, - onError = onError, + autoStart = autoStart, + onError = onError ) val stepCounterSensorState = remember { mutableStateOf(StepCounterSensorState()) } @@ -69,9 +88,11 @@ fun rememberStepCounterSensorState( stepCount = sensorStateValues[0], isAvailable = sensorState.isAvailable, accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } - }, + } ) return stepCounterSensorState.value diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/StepDetectorSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/StepDetectorSensorState.kt index 085af17..d8b2fa6 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/StepDetectorSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/StepDetectorSensorState.kt @@ -18,7 +18,9 @@ class StepDetectorSensorState internal constructor( val stepCount: Float = 0f, val isAvailable: Boolean = false, val accuracy: Int = 0, -) { + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is StepDetectorSensorState) return false @@ -26,6 +28,8 @@ class StepDetectorSensorState internal constructor( if (stepCount != other.stepCount) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -34,29 +38,44 @@ class StepDetectorSensorState internal constructor( var result = stepCount.hashCode() result = 31 * result + isAvailable.hashCode() result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } override fun toString(): String { - return "StepDetectorSensorState(stepCount=$stepCount isAvailable=$isAvailable, accuracy=$accuracy)" + return "StepDetectorSensorState(stepCount=$stepCount isAvailable=$isAvailable," + + " accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() } } /** * Creates and [remember]s an instance of [StepDetectorSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberStepDetectorSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, - onError: (throwable: Throwable) -> Unit = {}, + onError: (throwable: Throwable) -> Unit = {} ): StepDetectorSensorState { val sensorState = rememberSensorState( sensorType = SensorType.StepDetector, sensorDelay = sensorDelay, - onError = onError, + autoStart = autoStart, + onError = onError ) val stepDetectorSensorState = remember { mutableStateOf(StepDetectorSensorState()) } @@ -69,9 +88,11 @@ fun rememberStepDetectorSensorState( stepCount = sensorStateValues[0], isAvailable = sensorState.isAvailable, accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } - }, + } ) return stepDetectorSensorState.value diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedAccelerometerSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedAccelerometerSensorState.kt index a6bb276..8d8dbe7 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedAccelerometerSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedAccelerometerSensorState.kt @@ -39,8 +39,10 @@ class UncalibratedAccelerometerSensorState internal constructor( val yBias: Float = 0f, val zBias: Float = 0f, val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is UncalibratedAccelerometerSensorState) return false @@ -53,6 +55,8 @@ class UncalibratedAccelerometerSensorState internal constructor( if (zBias != other.zBias) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -64,8 +68,10 @@ class UncalibratedAccelerometerSensorState internal constructor( result = 31 * result + xBias.hashCode() result = 31 * result + yBias.hashCode() result = 31 * result + zBias.hashCode() - result = 31 * result + isAvailable.hashCode() result = 31 * result + accuracy.hashCode() + result = 31 * result + isAvailable.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } @@ -74,24 +80,35 @@ class UncalibratedAccelerometerSensorState internal constructor( "zForce=$zForce, xBiased=$xBias, yBiased=$yBias, zBiased=$zBias," + "isAvailable=$isAvailable, accuracy=$accuracy)" } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } } /** * Creates and [remember]s an instance of * [UncalibratedAccelerometerSensorState]. - * - * @param sensorDelay The rate at which the raw sensor data should be - * received. Defaults to [SensorDelay.Normal]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. + * @param sensorDelay The rate at which the raw sensor data should be received. Defaults to + * [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberUncalibratedAccelerometerSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, onError: (throwable: Throwable) -> Unit = {} ): UncalibratedAccelerometerSensorState { val sensorState = rememberSensorState( sensorType = SensorType.AccelerometerUncalibrated, sensorDelay = sensorDelay, + autoStart = autoStart, onError = onError ) val uncalibratedAccelerometerSensorState = @@ -110,7 +127,9 @@ fun rememberUncalibratedAccelerometerSensorState( yBias = sensorStateValues[4], zBias = sensorStateValues[5], isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedGyroscopeSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedGyroscopeSensorState.kt index 81f59de..bd9b90d 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedGyroscopeSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedGyroscopeSensorState.kt @@ -31,8 +31,10 @@ class UncalibratedGyroscopeSensorState internal constructor( val yBias: Float = 0f, val zBias: Float = 0f, val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is UncalibratedGyroscopeSensorState) return false @@ -44,6 +46,9 @@ class UncalibratedGyroscopeSensorState internal constructor( if (yBias != other.yBias) return false if (zBias != other.zBias) return false if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -56,6 +61,9 @@ class UncalibratedGyroscopeSensorState internal constructor( result = 31 * result + yBias.hashCode() result = 31 * result + zBias.hashCode() result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } @@ -64,23 +72,34 @@ class UncalibratedGyroscopeSensorState internal constructor( "zRotation=$zRotation," + "xBias=$xBias," + "yBias=$yBias," + "zBias=$zBias," + " isAvailable=$isAvailable, accuracy=$accuracy)" } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } } /** * Creates and [remember]s an instance of [GyroscopeSensorState]. - * - * @param sensorDelay The rate at which the raw sensor data should be - * received. Defaults to [SensorDelay.Normal]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. + * @param sensorDelay The rate at which the raw sensor data should be received. Defaults to + * [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberUncalibratedGyroscopeSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, onError: (throwable: Throwable) -> Unit = {} ): UncalibratedGyroscopeSensorState { val sensorState = rememberSensorState( sensorType = SensorType.GyroscopeUncalibrated, sensorDelay = sensorDelay, + autoStart = autoStart, onError = onError ) val uncalibratedGyroscopeSensorState = @@ -99,7 +118,9 @@ fun rememberUncalibratedGyroscopeSensorState( yBias = sensorStateValues[4], zBias = sensorStateValues[5], isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedLimitedAxesAccelerometerSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedLimitedAxesAccelerometerSensorState.kt index f680709..9921b7c 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedLimitedAxesAccelerometerSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedLimitedAxesAccelerometerSensorState.kt @@ -33,8 +33,10 @@ class UncalibratedLimitedAxesAccelerometerSensorState internal constructor( val yAxisSupported: Boolean = false, val zAxisSupported: Boolean = false, val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is UncalibratedLimitedAxesAccelerometerSensorState) return false @@ -50,6 +52,8 @@ class UncalibratedLimitedAxesAccelerometerSensorState internal constructor( if (zAxisSupported != other.zAxisSupported) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -64,8 +68,10 @@ class UncalibratedLimitedAxesAccelerometerSensorState internal constructor( result = 31 * result + xAxisSupported.hashCode() result = 31 * result + yAxisSupported.hashCode() result = 31 * result + zAxisSupported.hashCode() + result = 31 * result + accuracy.hashCode() result = 31 * result + isAvailable.hashCode() - result = 31 * result + accuracy + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } @@ -75,25 +81,38 @@ class UncalibratedLimitedAxesAccelerometerSensorState internal constructor( "xAxisSupported=$xAxisSupported, yAxisSupported=$yAxisSupported, " + "zAxisSupported=$zAxisSupported, isAvailable=$isAvailable, accuracy=$accuracy)" } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } } /** * Creates and [remember]s an instance of [UncalibratedLimitedAxesAccelerometerSensorState]. - * @param sensorDelay The rate at which the raw sensor data should be received. - * Defaults to [SensorDelay.Normal]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. + * @param sensorDelay The rate at which the raw sensor data should be received. Defaults to + * [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberUncalibratedLimitedAxesAccelerometerSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, onError: (throwable: Throwable) -> Unit = {} ): UncalibratedLimitedAxesAccelerometerSensorState { val sensorState = rememberSensorState( sensorType = SensorType.AccelerometerLimitedAxesUncalibrated, sensorDelay = sensorDelay, + autoStart = autoStart, onError = onError ) - val accelerometerSensorState = remember { mutableStateOf(UncalibratedLimitedAxesAccelerometerSensorState()) } + val accelerometerSensorState = + remember { mutableStateOf(UncalibratedLimitedAxesAccelerometerSensorState()) } LaunchedEffect( key1 = sensorState, @@ -111,7 +130,9 @@ fun rememberUncalibratedLimitedAxesAccelerometerSensorState( yAxisSupported = sensorStateValues[7] != 0f, zAxisSupported = sensorStateValues[8] != 0f, isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } } diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedMagneticFieldSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedMagneticFieldSensorState.kt index 81f56c4..f5a065c 100644 --- a/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedMagneticFieldSensorState.kt +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedMagneticFieldSensorState.kt @@ -28,8 +28,10 @@ class UncalibratedMagneticFieldSensorState internal constructor( val yBias: Float = 0f, val zBias: Float = 0f, val isAvailable: Boolean = false, - val accuracy: Int = 0 -) { + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is UncalibratedMagneticFieldSensorState) return false @@ -42,6 +44,8 @@ class UncalibratedMagneticFieldSensorState internal constructor( if (zBias != other.zBias) return false if (isAvailable != other.isAvailable) return false if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false return true } @@ -53,34 +57,49 @@ class UncalibratedMagneticFieldSensorState internal constructor( result = 31 * result + xBias.hashCode() result = 31 * result + yBias.hashCode() result = 31 * result + zBias.hashCode() - result = 31 * result + accuracy.hashCode() result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() return result } override fun toString(): String { - return "UncalibratedMagneticFieldSensorState(xStrength=$xStrength, yStrength=$yStrength, zStrength=$zStrength, " + - "xBias=$xBias, yBias=$yBias, zBias=$zBias, " + + return "UncalibratedMagneticFieldSensorState(xStrength=$xStrength, yStrength=$yStrength," + + " zStrength=$zStrength, xBias=$xBias, yBias=$yBias, zBias=$zBias, " + "isAvailable=$isAvailable, accuracy=$accuracy)" } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } } /** * Creates and [remember]s an instance of [UncalibratedMagneticFieldSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. * @param sensorDelay The rate at which the raw sensor data should be received. * Defaults to [SensorDelay.Normal]. * @param onError Callback invoked on every error state. */ @Composable fun rememberUncalibratedMagneticFieldSensorState( + autoStart: Boolean = true, sensorDelay: SensorDelay = SensorDelay.Normal, onError: (throwable: Throwable) -> Unit = {} ): UncalibratedMagneticFieldSensorState { val sensorState = rememberSensorState( sensorType = SensorType.MagneticFieldUncalibrated, sensorDelay = sensorDelay, + autoStart = autoStart, onError = onError ) + val uncalibratedMagneticFieldSensorState = remember { mutableStateOf(UncalibratedMagneticFieldSensorState()) } @@ -97,7 +116,9 @@ fun rememberUncalibratedMagneticFieldSensorState( yBias = sensorStateValues[4], zBias = sensorStateValues[5], isAvailable = sensorState.isAvailable, - accuracy = sensorState.accuracy + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening ) } } diff --git a/sample/src/main/java/com/mutualmobile/sample/ui/screens/sensorlist/SensorsListScreen.kt b/sample/src/main/java/com/mutualmobile/sample/ui/screens/sensorlist/SensorsListScreen.kt index 765a718..e41690a 100644 --- a/sample/src/main/java/com/mutualmobile/sample/ui/screens/sensorlist/SensorsListScreen.kt +++ b/sample/src/main/java/com/mutualmobile/sample/ui/screens/sensorlist/SensorsListScreen.kt @@ -30,27 +30,69 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import com.mutualmobile.composesensors.AccelerometerSensorState +import com.mutualmobile.composesensors.AmbientTemperatureSensorState +import com.mutualmobile.composesensors.GameRotationVectorSensorState +import com.mutualmobile.composesensors.GeomagneticRotationVectorSensorState +import com.mutualmobile.composesensors.GravitySensorState +import com.mutualmobile.composesensors.GyroscopeSensorState +import com.mutualmobile.composesensors.HeadTrackerSensorState +import com.mutualmobile.composesensors.HeadingSensorState +import com.mutualmobile.composesensors.HeartBeatSensorState +import com.mutualmobile.composesensors.HeartRateSensorState +import com.mutualmobile.composesensors.HingeAngleSensorState +import com.mutualmobile.composesensors.LightSensorState +import com.mutualmobile.composesensors.LimitedAxesAccelerometerSensorState +import com.mutualmobile.composesensors.LimitedAxesGyroscopeSensorState +import com.mutualmobile.composesensors.LinearAccelerationSensorState +import com.mutualmobile.composesensors.LowLatencyOffBodyDetectSensorState +import com.mutualmobile.composesensors.MagneticFieldSensorState +import com.mutualmobile.composesensors.MotionDetectSensorState +import com.mutualmobile.composesensors.PressureSensorState +import com.mutualmobile.composesensors.ProximitySensorState +import com.mutualmobile.composesensors.RotationVectorSensorState +import com.mutualmobile.composesensors.SignificantMotionSensorState +import com.mutualmobile.composesensors.StationaryDetectSensorState +import com.mutualmobile.composesensors.StepCounterSensorState +import com.mutualmobile.composesensors.StepDetectorSensorState +import com.mutualmobile.composesensors.UncalibratedAccelerometerSensorState +import com.mutualmobile.composesensors.UncalibratedGyroscopeSensorState +import com.mutualmobile.composesensors.UncalibratedLimitedAxesAccelerometerSensorState +import com.mutualmobile.composesensors.UncalibratedMagneticFieldSensorState import com.mutualmobile.composesensors.rememberAccelerometerSensorState import com.mutualmobile.composesensors.rememberAmbientTemperatureSensorState import com.mutualmobile.composesensors.rememberGameRotationVectorSensorState +import com.mutualmobile.composesensors.rememberGeomagneticRotationVectorSensorState import com.mutualmobile.composesensors.rememberGravitySensorState import com.mutualmobile.composesensors.rememberGyroscopeSensorState +import com.mutualmobile.composesensors.rememberHeadTrackerSensorState +import com.mutualmobile.composesensors.rememberHeadingSensorState +import com.mutualmobile.composesensors.rememberHeartBeatSensorState import com.mutualmobile.composesensors.rememberHeartRateSensorState import com.mutualmobile.composesensors.rememberHingeAngleSensorState import com.mutualmobile.composesensors.rememberLightSensorState +import com.mutualmobile.composesensors.rememberLimitedAxesAccelerometerSensorState import com.mutualmobile.composesensors.rememberLimitedAxesGyroscopeSensorState import com.mutualmobile.composesensors.rememberLinearAccelerationSensorState import com.mutualmobile.composesensors.rememberLowLatencyOffBodyDetectSensorState import com.mutualmobile.composesensors.rememberMagneticFieldSensorState +import com.mutualmobile.composesensors.rememberMotionDetectSensorState import com.mutualmobile.composesensors.rememberPressureSensorState import com.mutualmobile.composesensors.rememberProximitySensorState import com.mutualmobile.composesensors.rememberRotationVectorSensorState +import com.mutualmobile.composesensors.rememberSignificantMotionSensorState +import com.mutualmobile.composesensors.rememberStationaryDetectSensorState +import com.mutualmobile.composesensors.rememberStepCounterSensorState +import com.mutualmobile.composesensors.rememberStepDetectorSensorState +import com.mutualmobile.composesensors.rememberUncalibratedAccelerometerSensorState +import com.mutualmobile.composesensors.rememberUncalibratedGyroscopeSensorState +import com.mutualmobile.composesensors.rememberUncalibratedLimitedAxesAccelerometerSensorState import com.mutualmobile.composesensors.rememberUncalibratedMagneticFieldSensorState import com.mutualmobile.sample.R import com.mutualmobile.sample.ui.screens.sensorlist.components.CSButton @@ -59,6 +101,44 @@ import com.mutualmobile.sample.ui.screens.sensorlist.components.SensorItem import kotlinx.coroutines.delay import kotlinx.coroutines.launch +val sensorStates: List + @SuppressLint("NewApi") + @Composable + get() = listOf( + rememberAccelerometerSensorState(), + rememberMagneticFieldSensorState(), + rememberGyroscopeSensorState(), + rememberLightSensorState(), + rememberPressureSensorState(), + rememberProximitySensorState(), + rememberGravitySensorState(), + rememberLinearAccelerationSensorState(), + rememberRotationVectorSensorState(), + rememberAmbientTemperatureSensorState(), + rememberUncalibratedMagneticFieldSensorState(), + rememberGameRotationVectorSensorState(), + rememberUncalibratedGyroscopeSensorState(), + rememberSignificantMotionSensorState(onMotionEvent = {}), + rememberStepDetectorSensorState(), + rememberStepCounterSensorState(), + rememberGeomagneticRotationVectorSensorState(), + rememberHeartRateSensorState(), + rememberStationaryDetectSensorState(), + rememberMotionDetectSensorState(), + rememberHeartBeatSensorState(), + rememberLowLatencyOffBodyDetectSensorState(), + rememberUncalibratedAccelerometerSensorState(), + rememberHingeAngleSensorState(), + rememberHeadTrackerSensorState(), + rememberLimitedAxesAccelerometerSensorState(), + rememberLimitedAxesGyroscopeSensorState(), + rememberUncalibratedLimitedAxesAccelerometerSensorState(), + rememberHeadingSensorState() + ) + +// Please note this sample app will only work on Android 13 and above because we're also consuming +// sensor data that is only available on Android 13 and above and not making API level checks +// for every sensor yet. @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "NewApi") @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable @@ -66,25 +146,7 @@ fun SensorsListScreen() { var isTopBarTitleCollapsed by remember { mutableStateOf(false) } val pagerState = rememberPagerState() val coroutineScope = rememberCoroutineScope() - - // TODO: Keep this updated until a better option is found - val totalPageCount = rememberSaveable { 16 } - val accelerometerState = rememberAccelerometerSensorState() - val magneticFieldState = rememberMagneticFieldSensorState() - val gyroscopeState = rememberGyroscopeSensorState() - val lightState = rememberLightSensorState() - val pressureState = rememberPressureSensorState() - val proximityState = rememberProximitySensorState() - val gravityState = rememberGravitySensorState() - val linearAccelerationState = rememberLinearAccelerationSensorState() - val rotationVectorState = rememberRotationVectorSensorState() - val ambientTemperatureState = rememberAmbientTemperatureSensorState() - val uncalibratedMagneticFieldState = rememberUncalibratedMagneticFieldSensorState() - val gameRotationVectorState = rememberGameRotationVectorSensorState() - val heartRateState = rememberHeartRateSensorState() - val lowLatencyOffBodyDetectState = rememberLowLatencyOffBodyDetectSensorState() - val hingeAngleState = rememberHingeAngleSensorState() - val limitedAxesGyroscopeState = rememberLimitedAxesGyroscopeSensorState() + val totalSensorsCount = rememberUpdatedState(sensorStates.size) // Trigger TopBar animation once LaunchedEffect(Unit) { @@ -160,17 +222,17 @@ fun SensorsListScreen() { }, onLongClick = { coroutineScope.launch { - pagerState.scrollToPage(totalPageCount) + pagerState.scrollToPage(totalSensorsCount.value - 1) } }, position = CSButtonPosition.End, - enabled = pagerState.currentPage != totalPageCount - 1 + enabled = pagerState.currentPage != totalSensorsCount.value - 1 ) } } ) { HorizontalPager( - pageCount = totalPageCount, + pageCount = totalSensorsCount.value, state = pagerState, contentPadding = PaddingValues(32.dp), beyondBoundsPageCount = 1, @@ -181,145 +243,391 @@ fun SensorsListScreen() { pagerState.currentOffsetForPage(index) } } - SensorItem( - name = when (index) { - 0 -> "Accelerometer" - 1 -> "Magnetic Field" - 2 -> "Gyroscope" - 3 -> "Light" - 4 -> "Pressure" - 5 -> "Proximity" - 6 -> "Gravity" - 7 -> "Linear Acceleration" - 8 -> "Rotation Vector" - 9 -> "Ambient Temperature" - 10 -> "Magnetic Field (Uncalibrated)" - 11 -> "Game Rotation Vector" - 12 -> "Heart Rate" - 13 -> "Low Latency Off-Body Detection" - 14 -> "Hinge Angle" - 15 -> "Gyroscope (Limited Axes)" - else -> error("Invalid index '$index'") - }, - scrollProgress = scrollProgress, - imageRef = when (index) { - 0 -> R.drawable.accelerometer - 1 -> R.drawable.magnetic_field - 2 -> R.drawable.gyroscope - 3 -> R.drawable.light - 4 -> R.drawable.pressure - 5 -> R.drawable.proximity - 6 -> R.drawable.gravity - 7 -> R.drawable.linear_acceleration - 8 -> R.drawable.rotation_vector - 9 -> R.drawable.ambient_temperature - 10 -> R.drawable.magnetic_field - 11 -> R.drawable.game_rotation_vector - 12 -> R.drawable.heart_rate - 13 -> R.drawable.low_latency_off_body_detect - 14 -> R.drawable.hinge_angle - 15 -> R.drawable.gyroscope - else -> error("Invalid index '$index'") - }, - sensorValues = when (index) { - 0 -> mapOf( - "Force X" to accelerometerState.xForce, - "Force Y" to accelerometerState.yForce, - "Force Z" to accelerometerState.zForce + when (val state = sensorStates[index]) { + is AccelerometerSensorState -> { + SensorItem( + name = "Accelerometer", + scrollProgress = scrollProgress, + imageRef = R.drawable.accelerometer, + sensorValues = mapOf( + "Force X" to state.xForce, + "Force Y" to state.yForce, + "Force Z" to state.zForce + ), + isAvailable = state.isAvailable ) + } - 1 -> mapOf( - "Strength X" to magneticFieldState.xStrength, - "Strength Y" to magneticFieldState.yStrength, - "Strength Z" to magneticFieldState.zStrength + is MagneticFieldSensorState -> { + SensorItem( + name = "Magnetic Field", + scrollProgress = scrollProgress, + imageRef = R.drawable.magnetic_field, + sensorValues = mapOf( + "Strength X" to state.xStrength, + "Strength Y" to state.yStrength, + "Strength Z" to state.zStrength + ), + isAvailable = state.isAvailable ) + } - 2 -> mapOf( - "Rotation X" to gyroscopeState.xRotation, - "Rotation Y" to gyroscopeState.yRotation, - "Rotation Z" to gyroscopeState.zRotation + is GyroscopeSensorState -> { + SensorItem( + name = "Gyroscope", + scrollProgress = scrollProgress, + imageRef = R.drawable.gyroscope, + sensorValues = mapOf( + "Rotation X" to state.xRotation, + "Rotation Y" to state.yRotation, + "Rotation Z" to state.zRotation + ), + isAvailable = state.isAvailable ) + } - 3 -> mapOf("Illuminance" to lightState.illuminance) + is LightSensorState -> { + SensorItem( + name = "Light", + scrollProgress = scrollProgress, + imageRef = R.drawable.light, + sensorValues = mapOf("Illuminance" to state.illuminance), + isAvailable = state.isAvailable + ) + } - 4 -> mapOf("Pressure" to pressureState.pressure) + is PressureSensorState -> { + SensorItem( + name = "Pressure", + scrollProgress = scrollProgress, + imageRef = R.drawable.pressure, + sensorValues = mapOf("Pressure" to state.pressure), + isAvailable = state.isAvailable + ) + } - 5 -> mapOf("Distance" to proximityState.sensorDistance) + is ProximitySensorState -> { + SensorItem( + name = "Proximity", + scrollProgress = scrollProgress, + imageRef = R.drawable.proximity, + sensorValues = mapOf("Distance" to state.sensorDistance), + isAvailable = state.isAvailable + ) + } - 6 -> mapOf( - "Force X" to gravityState.xForce, - "Force Y" to gravityState.yForce, - "Force Z" to gravityState.zForce + is GravitySensorState -> { + SensorItem( + name = "Gravity", + scrollProgress = scrollProgress, + imageRef = R.drawable.gravity, + sensorValues = mapOf( + "Force X" to state.xForce, + "Force Y" to state.yForce, + "Force Z" to state.zForce + ), + isAvailable = state.isAvailable ) + } - 7 -> mapOf( - "Force X" to linearAccelerationState.xForce, - "Force Y" to linearAccelerationState.yForce, - "Force Z" to linearAccelerationState.zForce + is LinearAccelerationSensorState -> { + SensorItem( + name = "Linear Acceleration", + scrollProgress = scrollProgress, + imageRef = R.drawable.linear_acceleration, + sensorValues = mapOf( + "Force X" to state.xForce, + "Force Y" to state.yForce, + "Force Z" to state.zForce + ), + isAvailable = state.isAvailable ) + } - 8 -> mapOf( - "Vector X" to rotationVectorState.vectorX, - "Vector Y" to rotationVectorState.vectorY, - "Vector Z" to rotationVectorState.vectorZ, - "Scalar" to rotationVectorState.scalar, - "Heading Accuracy" to rotationVectorState.estimatedHeadingAccuracy + is RotationVectorSensorState -> { + SensorItem( + name = "Rotation Vector", + scrollProgress = scrollProgress, + imageRef = R.drawable.rotation_vector, + sensorValues = mapOf( + "Vector X" to state.vectorX, + "Vector Y" to state.vectorY, + "Vector Z" to state.vectorZ, + "Scalar" to state.scalar, + "Heading Accuracy" to state.estimatedHeadingAccuracy + ), + isAvailable = state.isAvailable ) + } - 9 -> mapOf("Temperature" to ambientTemperatureState.temperature) + is AmbientTemperatureSensorState -> { + SensorItem( + name = "Ambient Temperature", + scrollProgress = scrollProgress, + imageRef = R.drawable.ambient_temperature, + sensorValues = mapOf("Temperature" to state.temperature), + isAvailable = state.isAvailable + ) + } - 10 -> mapOf( - "Strength X" to uncalibratedMagneticFieldState.xStrength, - "Strength Y" to uncalibratedMagneticFieldState.yStrength, - "Strength Z" to uncalibratedMagneticFieldState.zStrength, - "Bias X" to uncalibratedMagneticFieldState.xBias, - "Bias Y" to uncalibratedMagneticFieldState.yBias, - "Bias Z" to uncalibratedMagneticFieldState.zBias + is UncalibratedMagneticFieldSensorState -> { + SensorItem( + name = "Magnetic Field (Uncalibrated)", + scrollProgress = scrollProgress, + imageRef = R.drawable.magnetic_field, + sensorValues = mapOf( + "Strength X" to state.xStrength, + "Strength Y" to state.yStrength, + "Strength Z" to state.zStrength, + "Bias X" to state.xBias, + "Bias Y" to state.yBias, + "Bias Z" to state.zBias + ), + isAvailable = state.isAvailable ) + } - 11 -> mapOf( - "Vector X" to gameRotationVectorState.vectorX, - "Vector Y" to gameRotationVectorState.vectorY, - "Vector Z" to gameRotationVectorState.vectorZ + is GameRotationVectorSensorState -> { + SensorItem( + name = "Game Rotation Vector", + scrollProgress = scrollProgress, + imageRef = R.drawable.game_rotation_vector, + sensorValues = mapOf( + "Vector X" to state.vectorX, + "Vector Y" to state.vectorY, + "Vector Z" to state.vectorZ + ), + isAvailable = state.isAvailable ) + } + + is UncalibratedGyroscopeSensorState -> { + SensorItem( + name = "Gyroscope (Uncalibrated)", + scrollProgress = scrollProgress, + imageRef = R.drawable.gyroscope, + sensorValues = mapOf( + "Rotation X" to state.xRotation, + "Rotation Y" to state.yRotation, + "Rotation Z" to state.zRotation, + "Bias X" to state.xBias, + "Bias Y" to state.yBias, + "Bias Z" to state.zBias + ), + isAvailable = state.isAvailable + ) + } - 12 -> mapOf("Heart Rate" to heartRateState.heartRate) + is SignificantMotionSensorState -> { + // Trigger motion event whenever its card is selected + LaunchedEffect(Unit) { + state.requestEventTrigger() + } + + SensorItem( + name = "Significant Motion", + scrollProgress = scrollProgress, + imageRef = R.drawable.significant_motion, + sensorValues = mapOf( + "Listening?" to state.isListening, + "Timestamp (ns)" to state.lastEventTimestamp + ), + isAvailable = state.isAvailable + ) + } - 13 -> mapOf("Is On Body?" to lowLatencyOffBodyDetectState.isDeviceOnBody) + is StepDetectorSensorState -> { + SensorItem( + name = "Step Detector", + scrollProgress = scrollProgress, + imageRef = R.drawable.step_detection, + sensorValues = mapOf("Step Count" to state.stepCount), + isAvailable = state.isAvailable + ) + } - 14 -> mapOf("Angle" to hingeAngleState.angle) + is StepCounterSensorState -> { + SensorItem( + name = "Step Counter", + scrollProgress = scrollProgress, + imageRef = R.drawable.step_counter, + sensorValues = mapOf("Step Count" to state.stepCount), + isAvailable = state.isAvailable + ) + } - 15 -> mapOf( - "Rotation X" to limitedAxesGyroscopeState.xRotation, - "Rotation Y" to limitedAxesGyroscopeState.yRotation, - "Rotation Z" to limitedAxesGyroscopeState.zRotation, - "X Supported?" to limitedAxesGyroscopeState.xAxisSupported, - "Y Supported?" to limitedAxesGyroscopeState.yAxisSupported, - "Z Supported?" to limitedAxesGyroscopeState.zAxisSupported + is GeomagneticRotationVectorSensorState -> { + SensorItem( + name = "Geomagnetic Rotation Vector", + scrollProgress = scrollProgress, + imageRef = R.drawable.geomagnetic_rotation_vector, + sensorValues = mapOf( + "Vector X" to state.vectorX, + "Vector Y" to state.vectorY, + "Vector Z" to state.vectorZ + ), + isAvailable = state.isAvailable ) + } - else -> error("Invalid index '$index'") - }, - isAvailable = when (index) { - 0 -> accelerometerState.isAvailable - 1 -> magneticFieldState.isAvailable - 2 -> gyroscopeState.isAvailable - 3 -> lightState.isAvailable - 4 -> pressureState.isAvailable - 5 -> proximityState.isAvailable - 6 -> gravityState.isAvailable - 7 -> linearAccelerationState.isAvailable - 8 -> rotationVectorState.isAvailable - 9 -> ambientTemperatureState.isAvailable - 10 -> uncalibratedMagneticFieldState.isAvailable - 11 -> gameRotationVectorState.isAvailable - 12 -> heartRateState.isAvailable - 13 -> lowLatencyOffBodyDetectState.isAvailable - 14 -> hingeAngleState.isAvailable - 15 -> limitedAxesGyroscopeState.isAvailable - else -> error("Invalid index '$index'") + is HeartRateSensorState -> { + SensorItem( + name = "Heart Rate", + scrollProgress = scrollProgress, + imageRef = R.drawable.heart_rate, + sensorValues = mapOf("Heart Rate" to state.heartRate), + isAvailable = state.isAvailable + ) } - ) + + is StationaryDetectSensorState -> { + SensorItem( + name = "Stationary Detect", + scrollProgress = scrollProgress, + imageRef = R.drawable.stationary_detect, + sensorValues = mapOf("Is device stationary?" to state.isDeviceStationary), + isAvailable = state.isAvailable + ) + } + + is MotionDetectSensorState -> { + SensorItem( + name = "Motion Detect", + scrollProgress = scrollProgress, + imageRef = R.drawable.motion_detect, + sensorValues = mapOf("Is device moving?" to state.isDeviceInMotion), + isAvailable = state.isAvailable + ) + } + + is HeartBeatSensorState -> { + SensorItem( + name = "Heart Beat", + scrollProgress = scrollProgress, + imageRef = R.drawable.heart_rate, + sensorValues = mapOf("Is confident peak?" to state.isConfidentPeak), + isAvailable = state.isAvailable + ) + } + + is LowLatencyOffBodyDetectSensorState -> { + SensorItem( + name = "Low Latency Off-Body Detect", + scrollProgress = scrollProgress, + imageRef = R.drawable.low_latency_off_body_detect, + sensorValues = mapOf("Is On Body?" to state.isDeviceOnBody), + isAvailable = state.isAvailable + ) + } + + is UncalibratedAccelerometerSensorState -> { + SensorItem( + name = "Accelerometer (Uncalibrated)", + scrollProgress = scrollProgress, + imageRef = R.drawable.accelerometer, + sensorValues = mapOf( + "Force X" to state.xForce, + "Force Y" to state.yForce, + "Force Z" to state.zForce, + "Bias X" to state.xBias, + "Bias Y" to state.yBias, + "Bias Z" to state.zBias + ), + isAvailable = state.isAvailable + ) + } + + is HingeAngleSensorState -> { + SensorItem( + name = "Hinge Angle", + scrollProgress = scrollProgress, + imageRef = R.drawable.hinge_angle, + sensorValues = mapOf("Angle" to state.angle), + isAvailable = state.isAvailable + ) + } + + is HeadTrackerSensorState -> { + SensorItem( + name = "Head Tracker", + scrollProgress = scrollProgress, + imageRef = R.drawable.head_tracker, + sensorValues = mapOf( + "Rotation X" to state.xRotation, + "Rotation Y" to state.yRotation, + "Rotation Z" to state.zRotation, + "Velocity X" to state.xAngularVelocity, + "Velocity Y" to state.yAngularVelocity, + "Velocity Z" to state.zAngularVelocity + ), + isAvailable = state.isAvailable + ) + } + + is LimitedAxesAccelerometerSensorState -> { + SensorItem( + name = "Accelerometer (Limited Axes)", + scrollProgress = scrollProgress, + imageRef = R.drawable.accelerometer, + sensorValues = mapOf( + "Force X" to state.xForce, + "Force Y" to state.yForce, + "Force Z" to state.zForce, + "X Supported?" to state.xAxisSupported, + "Y Supported?" to state.yAxisSupported, + "Z Supported?" to state.zAxisSupported + ), + isAvailable = state.isAvailable + ) + } + + is LimitedAxesGyroscopeSensorState -> { + SensorItem( + name = "Gyroscope (Limited Axes)", + scrollProgress = scrollProgress, + imageRef = R.drawable.gyroscope, + sensorValues = mapOf( + "Rotation X" to state.xRotation, + "Rotation Y" to state.yRotation, + "Rotation Z" to state.zRotation, + "X Supported?" to state.xAxisSupported, + "Y Supported?" to state.yAxisSupported, + "Z Supported?" to state.zAxisSupported + ), + isAvailable = state.isAvailable + ) + } + + is UncalibratedLimitedAxesAccelerometerSensorState -> { + SensorItem( + name = "Accelerometer (Uncalibrated - Limited Axes)", + scrollProgress = scrollProgress, + imageRef = R.drawable.accelerometer, + sensorValues = mapOf( + "Rotation X" to state.xForce, + "Rotation Y" to state.yForce, + "Rotation Z" to state.zForce, + "Bias X" to state.xBias, + "Bias Y" to state.yBias, + "Bias Z" to state.zBias, + "X Supported?" to state.xAxisSupported, + "Y Supported?" to state.yAxisSupported, + "Z Supported?" to state.zAxisSupported + ), + isAvailable = state.isAvailable + ) + } + + is HeadingSensorState -> { + SensorItem( + name = "Heading", + scrollProgress = scrollProgress, + imageRef = R.drawable.heading, + sensorValues = mapOf("Degrees" to state.degrees), + isAvailable = state.isAvailable + ) + } + } } } } diff --git a/sample/src/main/res/drawable/ambient_temperature.png b/sample/src/main/res/drawable/ambient_temperature.png deleted file mode 100644 index 53d0743..0000000 Binary files a/sample/src/main/res/drawable/ambient_temperature.png and /dev/null differ diff --git a/sample/src/main/res/drawable/ambient_temperature.webp b/sample/src/main/res/drawable/ambient_temperature.webp new file mode 100644 index 0000000..35ed02f Binary files /dev/null and b/sample/src/main/res/drawable/ambient_temperature.webp differ diff --git a/sample/src/main/res/drawable/geomagnetic_rotation_vector.webp b/sample/src/main/res/drawable/geomagnetic_rotation_vector.webp new file mode 100644 index 0000000..c5c0545 Binary files /dev/null and b/sample/src/main/res/drawable/geomagnetic_rotation_vector.webp differ diff --git a/sample/src/main/res/drawable/gravity.png b/sample/src/main/res/drawable/gravity.png deleted file mode 100644 index 5d183ff..0000000 Binary files a/sample/src/main/res/drawable/gravity.png and /dev/null differ diff --git a/sample/src/main/res/drawable/gravity.webp b/sample/src/main/res/drawable/gravity.webp new file mode 100644 index 0000000..ad30126 Binary files /dev/null and b/sample/src/main/res/drawable/gravity.webp differ diff --git a/sample/src/main/res/drawable/gyroscope.png b/sample/src/main/res/drawable/gyroscope.png deleted file mode 100644 index 5b464ff..0000000 Binary files a/sample/src/main/res/drawable/gyroscope.png and /dev/null differ diff --git a/sample/src/main/res/drawable/gyroscope.webp b/sample/src/main/res/drawable/gyroscope.webp new file mode 100644 index 0000000..9071b86 Binary files /dev/null and b/sample/src/main/res/drawable/gyroscope.webp differ diff --git a/sample/src/main/res/drawable/head_tracker.webp b/sample/src/main/res/drawable/head_tracker.webp new file mode 100644 index 0000000..a34cc88 Binary files /dev/null and b/sample/src/main/res/drawable/head_tracker.webp differ diff --git a/sample/src/main/res/drawable/heading.webp b/sample/src/main/res/drawable/heading.webp new file mode 100644 index 0000000..e9409ca Binary files /dev/null and b/sample/src/main/res/drawable/heading.webp differ diff --git a/sample/src/main/res/drawable/heart_rate.png b/sample/src/main/res/drawable/heart_rate.png deleted file mode 100644 index 0b9cde3..0000000 Binary files a/sample/src/main/res/drawable/heart_rate.png and /dev/null differ diff --git a/sample/src/main/res/drawable/heart_rate.webp b/sample/src/main/res/drawable/heart_rate.webp new file mode 100644 index 0000000..4189036 Binary files /dev/null and b/sample/src/main/res/drawable/heart_rate.webp differ diff --git a/sample/src/main/res/drawable/linear_acceleration.png b/sample/src/main/res/drawable/linear_acceleration.png deleted file mode 100644 index d5f6f6b..0000000 Binary files a/sample/src/main/res/drawable/linear_acceleration.png and /dev/null differ diff --git a/sample/src/main/res/drawable/linear_acceleration.webp b/sample/src/main/res/drawable/linear_acceleration.webp new file mode 100644 index 0000000..90857b1 Binary files /dev/null and b/sample/src/main/res/drawable/linear_acceleration.webp differ diff --git a/sample/src/main/res/drawable/low_latency_off_body_detect.jpg b/sample/src/main/res/drawable/low_latency_off_body_detect.jpg deleted file mode 100644 index 0b1cf40..0000000 Binary files a/sample/src/main/res/drawable/low_latency_off_body_detect.jpg and /dev/null differ diff --git a/sample/src/main/res/drawable/low_latency_off_body_detect.webp b/sample/src/main/res/drawable/low_latency_off_body_detect.webp new file mode 100644 index 0000000..26466ee Binary files /dev/null and b/sample/src/main/res/drawable/low_latency_off_body_detect.webp differ diff --git a/sample/src/main/res/drawable/magnetic_field.png b/sample/src/main/res/drawable/magnetic_field.png deleted file mode 100644 index ddf6662..0000000 Binary files a/sample/src/main/res/drawable/magnetic_field.png and /dev/null differ diff --git a/sample/src/main/res/drawable/magnetic_field.webp b/sample/src/main/res/drawable/magnetic_field.webp new file mode 100644 index 0000000..27b4fc0 Binary files /dev/null and b/sample/src/main/res/drawable/magnetic_field.webp differ diff --git a/sample/src/main/res/drawable/motion_detect.webp b/sample/src/main/res/drawable/motion_detect.webp new file mode 100644 index 0000000..450f9e7 Binary files /dev/null and b/sample/src/main/res/drawable/motion_detect.webp differ diff --git a/sample/src/main/res/drawable/pressure.jpg b/sample/src/main/res/drawable/pressure.jpg deleted file mode 100644 index f30511f..0000000 Binary files a/sample/src/main/res/drawable/pressure.jpg and /dev/null differ diff --git a/sample/src/main/res/drawable/pressure.webp b/sample/src/main/res/drawable/pressure.webp new file mode 100644 index 0000000..07ef003 Binary files /dev/null and b/sample/src/main/res/drawable/pressure.webp differ diff --git a/sample/src/main/res/drawable/rotation_vector.png b/sample/src/main/res/drawable/rotation_vector.png deleted file mode 100644 index fb6474b..0000000 Binary files a/sample/src/main/res/drawable/rotation_vector.png and /dev/null differ diff --git a/sample/src/main/res/drawable/rotation_vector.webp b/sample/src/main/res/drawable/rotation_vector.webp new file mode 100644 index 0000000..d1b3f8c Binary files /dev/null and b/sample/src/main/res/drawable/rotation_vector.webp differ diff --git a/sample/src/main/res/drawable/significant_motion.webp b/sample/src/main/res/drawable/significant_motion.webp new file mode 100644 index 0000000..cbd702e Binary files /dev/null and b/sample/src/main/res/drawable/significant_motion.webp differ diff --git a/sample/src/main/res/drawable/stationary_detect.webp b/sample/src/main/res/drawable/stationary_detect.webp new file mode 100644 index 0000000..c92f090 Binary files /dev/null and b/sample/src/main/res/drawable/stationary_detect.webp differ diff --git a/sample/src/main/res/drawable/step_counter.webp b/sample/src/main/res/drawable/step_counter.webp new file mode 100644 index 0000000..45b5cfb Binary files /dev/null and b/sample/src/main/res/drawable/step_counter.webp differ diff --git a/sample/src/main/res/drawable/step_detection.webp b/sample/src/main/res/drawable/step_detection.webp new file mode 100644 index 0000000..401f436 Binary files /dev/null and b/sample/src/main/res/drawable/step_detection.webp differ