Implement new DataType/DataPoint
Relnote: """Updated how data is modeled
The data model and how DataTypes, DataPoints, and their underlying
values are represented has been overhauled. The top level impact is
that APIs are now much more explicit and type safe.
Previously developers may have written code that looked like:
```
exerciseUpdate.latestMetrics[DataType.LOCATION]?.forEach {
val loc = it.value.asDoubleArray()
val lat = loc[DataPoints.LOCATION_DATA_POINT_LATITUDE_INDEX]
val lon = loc[DataPoints.LOCATION_DATA_POINT_LONGITUDE_INDEX]
val alt = loc[DataPoints.LOCATION_DATA_POINT_ALTITUDE_INDEX]
println("($lat,$lon,$alt) @ ${it.startDurationFromBoot}")
}
```
With these changes, the equivalent code would now be:
```
exerciseUpdate.latestMetrics.getData(DataType.LOCATION).forEach {
val loc = it.value
val time = it.timeDurationFromBoot
println("loc = [${loc.latitude}, ${loc.longitude}, ${loc.altitude}] @ $time")
}
```
There is now sufficient information embedded in the definition of a
`DataType` and `DataPoint` for the compiler and IDE to know that
`getData(DataType.Location)` will return a
`List<SampleDataPoint<LocationData>>`. That enables type safety
inside the IDE and at compile time rather than at runtime.
DataType is now generic on the type of `DataPoint` that can represent it:
* DeltaDataType
* NumericDeltaDataType
* AggregateDataType (numeric by default)
DataPoint<D : DataType> is now generic and has subclasses
* IntervalDataPoint<DeltaDataType> (for e.g. DataType.STEPS)
* Previously: DataPoint for a DataType with TimeType.INTERVAL
* SampleDataPoint<DeltaDataType> (DataType.HEART_RATE_BPM)
* Previously: DataPoint for a DataType with TimeType.SAMPLE
* CumulativeDataPoint<AggregateDataType> (DataType.STEPS_TOTAL)
* Previously: CumulativeDataPoint for DataType with TimeType.INTERVAL
* StatisticalDataPoint<AggregateDataType> (DataType.HEART_RATE_BPM_STATS)
* Previously: StatisticalDataPoint for DataType with TimeType.SAMPLE
`DataPointContainer` has been added. Where previously APIs returned
a map from DataType to DataPoint, they now return a `DataPointContainer`
object which enables them to access DataPoints in a type safe way
through `getData` methods.
Location:
Previously `DataType.LOCATION` was a `DoubleArray` with
Latitude/Longitude/Altitude/Bearing available at indexes specified
by constants, with Altitude/Bearing being optional and
alternatively containing `Double.MAX_VALUE` if unavailable.
This has now been changed such that DataType.LOCATION now maps to
`LocationData` which has latitude/longitude/altitude/bearing
properties, with altitude and bearing being nullable to indicate
absence.
`AchievedExerciseGoal` removed. It only contained the
`exerciseGoal` property which is now inlined.
`AggregateDataPoints` class has been removed. Utility functions
have been moved to `DataPoints`
ExerciseUpdate: Since ExerciseType is required, the builder now
requires this to be specified in its constructor.
"""
Test: ./gradlew :health:health-services-client:test
Bug: 227475943
Change-Id: I287a816075bf721c9cec2471a5032366f70eee4f
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/HealthEvent.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/HealthEvent.kt
index 5f73d62..5650cff 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/HealthEvent.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/HealthEvent.kt
@@ -17,6 +17,7 @@
package androidx.health.services.client.data
import androidx.health.services.client.proto.DataProto
+import androidx.health.services.client.proto.DataProto.HealthEvent.MetricsEntry
import java.time.Instant
/** Represents a user's health event. */
@@ -28,7 +29,7 @@
public val eventTime: Instant,
/** Gets metrics associated to the event. */
- public val metrics: Map<DataType, List<DataPoint>>,
+ public val metrics: DataPointContainer,
) {
/** Health event types. */
@@ -75,26 +76,77 @@
) : this(
Type.fromProto(proto.type),
Instant.ofEpochMilli(proto.eventTimeEpochMs),
- proto
- .metricsList
- .map { entry -> DataType(entry.dataType) to entry.dataPointsList.map { DataPoint(it) } }
- .toMap()
+ fromHealthEventProto(proto)
)
internal val proto: DataProto.HealthEvent by lazy {
DataProto.HealthEvent.newBuilder()
.setType(type.toProto())
.setEventTimeEpochMs(eventTime.toEpochMilli())
- .addAllMetrics(
- metrics
- .map { entry ->
- DataProto.HealthEvent.MetricsEntry.newBuilder()
- .setDataType(entry.key.proto)
- .addAllDataPoints(entry.value.map { it.proto })
- .build()
- }
- .sortedBy { it.dataType.name } // Required to ensure equals() works
- )
+ .addAllMetrics(toEventProtoList(metrics))
.build()
}
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is HealthEvent) return false
+ if (type != other.type) return false
+ if (eventTime != other.eventTime) return false
+ if (metrics != other.metrics) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = type.hashCode()
+ result = 31 * result + eventTime.hashCode()
+ result = 31 * result + metrics.hashCode()
+ return result
+ }
+
+ internal companion object {
+ internal fun toEventProtoList(container: DataPointContainer): List<MetricsEntry> {
+ val list = mutableListOf<MetricsEntry>()
+
+ for (entry in container.dataPoints) {
+ if (entry.value.isEmpty()) {
+ continue
+ }
+
+ when (entry.key.timeType) {
+ DataType.TimeType.SAMPLE -> {
+ list.add(
+ MetricsEntry.newBuilder()
+ .setDataType(entry.key.proto)
+ .addAllDataPoints(entry.value.map { (it as SampleDataPoint).proto })
+ .build()
+ )
+ }
+ DataType.TimeType.INTERVAL -> {
+ list.add(
+ MetricsEntry.newBuilder()
+ .setDataType(entry.key.proto)
+ .addAllDataPoints(entry.value.map {
+ (it as IntervalDataPoint).proto
+ })
+ .build()
+ )
+ }
+ }
+ }
+ return list.sortedBy { it.dataType.name } // Required to ensure equals() works
+ }
+
+ internal fun fromHealthEventProto(
+ proto: DataProto.HealthEvent
+ ): DataPointContainer {
+ val dataTypeToDataPoints: Map<DataType<*, *>, List<DataPoint<*>>> =
+ proto.metricsList.associate { entry ->
+ DataType.deltaFromProto(entry.dataType) to entry.dataPointsList.map {
+ DataPoint.fromProto(it)
+ }
+ }
+ return DataPointContainer(dataTypeToDataPoints)
+ }
+ }
}
\ No newline at end of file