[go: nahoru, domu]

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