RecordToProtoConverters.kt

/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.health.connect.client.impl.converters.records

import androidx.health.connect.client.records.ActiveCaloriesBurned
import androidx.health.connect.client.records.ActiveEnergyBurned
import androidx.health.connect.client.records.ActivityEvent
import androidx.health.connect.client.records.ActivityLap
import androidx.health.connect.client.records.ActivitySession
import androidx.health.connect.client.records.BasalBodyTemperature
import androidx.health.connect.client.records.BasalMetabolicRate
import androidx.health.connect.client.records.BloodGlucose
import androidx.health.connect.client.records.BloodPressure
import androidx.health.connect.client.records.BodyFat
import androidx.health.connect.client.records.BodyTemperature
import androidx.health.connect.client.records.BodyWaterMass
import androidx.health.connect.client.records.BoneMass
import androidx.health.connect.client.records.CervicalMucus
import androidx.health.connect.client.records.CervicalPosition
import androidx.health.connect.client.records.CyclingPedalingCadenceSeries
import androidx.health.connect.client.records.Distance
import androidx.health.connect.client.records.ElevationGained
import androidx.health.connect.client.records.FloorsClimbed
import androidx.health.connect.client.records.HeartRateSeries
import androidx.health.connect.client.records.HeartRateVariabilityDifferentialIndex
import androidx.health.connect.client.records.HeartRateVariabilityRmssd
import androidx.health.connect.client.records.HeartRateVariabilityS
import androidx.health.connect.client.records.HeartRateVariabilitySd2
import androidx.health.connect.client.records.HeartRateVariabilitySdann
import androidx.health.connect.client.records.HeartRateVariabilitySdnn
import androidx.health.connect.client.records.HeartRateVariabilitySdnnIndex
import androidx.health.connect.client.records.HeartRateVariabilitySdsd
import androidx.health.connect.client.records.HeartRateVariabilityTinn
import androidx.health.connect.client.records.Height
import androidx.health.connect.client.records.HipCircumference
import androidx.health.connect.client.records.Hydration
import androidx.health.connect.client.records.LeanBodyMass
import androidx.health.connect.client.records.Menstruation
import androidx.health.connect.client.records.Nutrition
import androidx.health.connect.client.records.OvulationTest
import androidx.health.connect.client.records.OxygenSaturation
import androidx.health.connect.client.records.PowerSeries
import androidx.health.connect.client.records.Record
import androidx.health.connect.client.records.Repetitions
import androidx.health.connect.client.records.RespiratoryRate
import androidx.health.connect.client.records.RestingHeartRate
import androidx.health.connect.client.records.SeriesRecord
import androidx.health.connect.client.records.SexualActivity
import androidx.health.connect.client.records.SleepSession
import androidx.health.connect.client.records.SleepStage
import androidx.health.connect.client.records.SpeedSeries
import androidx.health.connect.client.records.Steps
import androidx.health.connect.client.records.StepsCadenceSeries
import androidx.health.connect.client.records.SwimmingStrokes
import androidx.health.connect.client.records.TotalCaloriesBurned
import androidx.health.connect.client.records.TotalEnergyBurned
import androidx.health.connect.client.records.Vo2Max
import androidx.health.connect.client.records.WaistCircumference
import androidx.health.connect.client.records.Weight
import androidx.health.connect.client.records.WheelchairPushes
import androidx.health.platform.client.proto.DataProto

/** Converts public API object into internal proto for ipc. */
fun Record.toProto(): DataProto.DataPoint =
    when (this) {
        is BasalBodyTemperature ->
            instantaneousProto()
                .setDataType(protoDataType("BasalBodyTemperature"))
                .apply {
                    putValues("temperature", doubleVal(temperatureDegreesCelsius))
                    measurementLocation?.let { putValues("measurementLocation", enumVal(it)) }
                }
                .build()
        is BasalMetabolicRate ->
            instantaneousProto()
                .setDataType(protoDataType("BasalMetabolicRate"))
                .apply { putValues("bmr", doubleVal(kcalPerDay)) }
                .build()
        is BloodGlucose ->
            instantaneousProto()
                .setDataType(protoDataType("BloodGlucose"))
                .apply {
                    putValues("level", doubleVal(levelMillimolesPerLiter))
                    specimenSource?.let { putValues("specimenSource", enumVal(it)) }
                    mealType?.let { putValues("mealType", enumVal(it)) }
                    relationToMeal?.let { putValues("relationToMeal", enumVal(it)) }
                }
                .build()
        is BloodPressure ->
            instantaneousProto()
                .setDataType(protoDataType("BloodPressure"))
                .apply {
                    putValues("systolic", doubleVal(systolicMillimetersOfMercury))
                    putValues("diastolic", doubleVal(diastolicMillimetersOfMercury))
                    bodyPosition?.let { putValues("bodyPosition", enumVal(it)) }
                    measurementLocation?.let { putValues("measurementLocation", enumVal(it)) }
                }
                .build()
        is BodyFat ->
            instantaneousProto()
                .setDataType(protoDataType("BodyFat"))
                .apply { putValues("percentage", doubleVal(percentage.toDouble())) }
                .build()
        is BodyTemperature ->
            instantaneousProto()
                .setDataType(protoDataType("BodyTemperature"))
                .apply {
                    putValues("temperature", doubleVal(temperatureDegreesCelsius))
                    measurementLocation?.let { putValues("measurementLocation", enumVal(it)) }
                }
                .build()
        is BodyWaterMass ->
            instantaneousProto()
                .setDataType(protoDataType("BodyWaterMass"))
                .apply { putValues("mass", doubleVal(massKg)) }
                .build()
        is BoneMass ->
            instantaneousProto()
                .setDataType(protoDataType("BoneMass"))
                .apply { putValues("mass", doubleVal(massKg)) }
                .build()
        is CervicalMucus ->
            instantaneousProto()
                .setDataType(protoDataType("CervicalMucus"))
                .apply {
                    texture?.let { putValues("texture", enumVal(it)) }
                    amount?.let { putValues("amount", enumVal(it)) }
                }
                .build()
        is CervicalPosition ->
            instantaneousProto()
                .setDataType(protoDataType("CervicalPosition"))
                .apply {
                    position?.let { putValues("position", enumVal(it)) }
                    dilation?.let { putValues("dilation", enumVal(it)) }
                    firmness?.let { putValues("firmness", enumVal(it)) }
                }
                .build()
        is CyclingPedalingCadenceSeries ->
            toProto(dataTypeName = "CyclingPedalingCadenceSeries") { sample ->
                DataProto.SeriesValue.newBuilder()
                    .putValues("rpm", doubleVal(sample.revolutionsPerMinute))
                    .setInstantTimeMillis(sample.time.toEpochMilli())
                    .build()
            }
        is HeartRateSeries ->
            toProto(dataTypeName = "HeartRateSeries") { sample ->
                DataProto.SeriesValue.newBuilder()
                    .putValues("bpm", longVal(sample.beatsPerMinute))
                    .setInstantTimeMillis(sample.time.toEpochMilli())
                    .build()
            }
        is Height ->
            instantaneousProto()
                .setDataType(protoDataType("Height"))
                .apply { putValues("height", doubleVal(heightMeters)) }
                .build()
        is HipCircumference ->
            instantaneousProto()
                .setDataType(protoDataType("HipCircumference"))
                .apply { putValues("circumference", doubleVal(circumferenceMeters)) }
                .build()
        is HeartRateVariabilityDifferentialIndex ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilityDifferentialIndex"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilityRmssd ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilityRmssd"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilityS ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilityS"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilitySd2 ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilitySd2"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilitySdann ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilitySdann"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilitySdnnIndex ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilitySdnnIndex"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilitySdnn ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilitySdnn"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilitySdsd ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilitySdsd"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilityTinn ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilityTinn"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is LeanBodyMass ->
            instantaneousProto()
                .setDataType(protoDataType("LeanBodyMass"))
                .apply { putValues("mass", doubleVal(massKg)) }
                .build()
        is Menstruation ->
            instantaneousProto()
                .setDataType(protoDataType("Menstruation"))
                .apply { flow?.let { putValues("flow", enumVal(it)) } }
                .build()
        is OvulationTest ->
            instantaneousProto()
                .setDataType(protoDataType("OvulationTest"))
                .apply { putValues("result", enumVal(result)) }
                .build()
        is OxygenSaturation ->
            instantaneousProto()
                .setDataType(protoDataType("OxygenSaturation"))
                .apply { putValues("percentage", doubleVal(percentage.toDouble())) }
                .build()
        is PowerSeries ->
            toProto(dataTypeName = "PowerSeries") { sample ->
                DataProto.SeriesValue.newBuilder()
                    .putValues("power", doubleVal(sample.watts))
                    .setInstantTimeMillis(sample.time.toEpochMilli())
                    .build()
            }
        is RespiratoryRate ->
            instantaneousProto()
                .setDataType(protoDataType("RespiratoryRate"))
                .apply { putValues("rate", doubleVal(rate)) }
                .build()
        is RestingHeartRate ->
            instantaneousProto()
                .setDataType(protoDataType("RestingHeartRate"))
                .apply { putValues("bpm", longVal(beatsPerMinute)) }
                .build()
        is SexualActivity ->
            instantaneousProto()
                .setDataType(protoDataType("SexualActivity"))
                .apply { protectionUsed?.let { putValues("protectionUsed", enumVal(it)) } }
                .build()
        is SpeedSeries ->
            toProto(dataTypeName = "SpeedSeries") { sample ->
                DataProto.SeriesValue.newBuilder()
                    .putValues("speed", doubleVal(sample.metersPerSecond))
                    .setInstantTimeMillis(sample.time.toEpochMilli())
                    .build()
            }
        is StepsCadenceSeries ->
            toProto(dataTypeName = "StepsCadenceSeries") { sample ->
                DataProto.SeriesValue.newBuilder()
                    .putValues("rate", doubleVal(sample.rate))
                    .setInstantTimeMillis(sample.time.toEpochMilli())
                    .build()
            }
        is Vo2Max ->
            instantaneousProto()
                .setDataType(protoDataType("Vo2Max"))
                .apply {
                    putValues("vo2", doubleVal(vo2MillilitersPerMinuteKilogram))
                    measurementMethod?.let { putValues("measurementMethod", enumVal(it)) }
                }
                .build()
        is WaistCircumference ->
            instantaneousProto()
                .setDataType(protoDataType("WaistCircumference"))
                .apply { putValues("circumference", doubleVal(circumferenceMeters)) }
                .build()
        is Weight ->
            instantaneousProto()
                .setDataType(protoDataType("Weight"))
                .apply { putValues("weight", doubleVal(weightKg)) }
                .build()
        is ActiveCaloriesBurned ->
            intervalProto()
                .setDataType(protoDataType("ActiveCaloriesBurned"))
                .apply { putValues("energy", doubleVal(energyKcal)) }
                .build()
        is ActiveEnergyBurned ->
            intervalProto()
                .setDataType(protoDataType("ActiveEnergyBurned"))
                .apply { putValues("energy", doubleVal(energyKcal)) }
                .build()
        is ActivityEvent ->
            intervalProto()
                .setDataType(protoDataType("ActivityEvent"))
                .apply { putValues("eventType", enumVal(eventType)) }
                .build()
        is ActivityLap ->
            intervalProto()
                .setDataType(protoDataType("ActivityLap"))
                .apply {
                    if (lengthMeters > 0) {
                        putValues("length", doubleVal(lengthMeters))
                    }
                }
                .build()
        is ActivitySession ->
            intervalProto()
                .setDataType(protoDataType("ActivitySession"))
                .apply {
                    putValues("activityType", enumVal(activityType))
                    title?.let { putValues("title", stringVal(it)) }
                    notes?.let { putValues("notes", stringVal(it)) }
                }
                .build()
        is Distance ->
            intervalProto()
                .setDataType(protoDataType("Distance"))
                .apply { putValues("distance", doubleVal(distanceMeters)) }
                .build()
        is ElevationGained ->
            intervalProto()
                .setDataType(protoDataType("ElevationGained"))
                .apply { putValues("elevation", doubleVal(elevationMeters)) }
                .build()
        is FloorsClimbed ->
            intervalProto()
                .setDataType(protoDataType("FloorsClimbed"))
                .apply { putValues("floors", doubleVal(floors)) }
                .build()
        is Hydration ->
            intervalProto()
                .setDataType(protoDataType("Hydration"))
                .apply { putValues("volume", doubleVal(volumeLiters)) }
                .build()
        is Nutrition ->
            intervalProto()
                .setDataType(protoDataType("Nutrition"))
                .apply {
                    if (biotinGrams > 0) {
                        putValues("biotin", doubleVal(biotinGrams))
                    }
                    if (caffeineGrams > 0) {
                        putValues("caffeine", doubleVal(caffeineGrams))
                    }
                    if (calciumGrams > 0) {
                        putValues("calcium", doubleVal(calciumGrams))
                    }
                    if (kcal > 0) {
                        putValues("calories", doubleVal(kcal))
                    }
                    if (kcalFromFat > 0) {
                        putValues("caloriesFromFat", doubleVal(kcalFromFat))
                    }
                    if (chlorideGrams > 0) {
                        putValues("chloride", doubleVal(chlorideGrams))
                    }
                    if (cholesterolGrams > 0) {
                        putValues("cholesterol", doubleVal(cholesterolGrams))
                    }
                    if (chromiumGrams > 0) {
                        putValues("chromium", doubleVal(chromiumGrams))
                    }
                    if (copperGrams > 0) {
                        putValues("copper", doubleVal(copperGrams))
                    }
                    if (dietaryFiberGrams > 0) {
                        putValues("dietaryFiber", doubleVal(dietaryFiberGrams))
                    }
                    if (folateGrams > 0) {
                        putValues("folate", doubleVal(folateGrams))
                    }
                    if (folicAcidGrams > 0) {
                        putValues("folicAcid", doubleVal(folicAcidGrams))
                    }
                    if (iodineGrams > 0) {
                        putValues("iodine", doubleVal(iodineGrams))
                    }
                    if (ironGrams > 0) {
                        putValues("iron", doubleVal(ironGrams))
                    }
                    if (magnesiumGrams > 0) {
                        putValues("magnesium", doubleVal(magnesiumGrams))
                    }
                    if (manganeseGrams > 0) {
                        putValues("manganese", doubleVal(manganeseGrams))
                    }
                    if (molybdenumGrams > 0) {
                        putValues("molybdenum", doubleVal(molybdenumGrams))
                    }
                    if (monounsaturatedFatGrams > 0) {
                        putValues("monounsaturatedFat", doubleVal(monounsaturatedFatGrams))
                    }
                    if (niacinGrams > 0) {
                        putValues("niacin", doubleVal(niacinGrams))
                    }
                    if (pantothenicAcidGrams > 0) {
                        putValues("pantothenicAcid", doubleVal(pantothenicAcidGrams))
                    }
                    if (phosphorusGrams > 0) {
                        putValues("phosphorus", doubleVal(phosphorusGrams))
                    }
                    if (polyunsaturatedFatGrams > 0) {
                        putValues("polyunsaturatedFat", doubleVal(polyunsaturatedFatGrams))
                    }
                    if (potassiumGrams > 0) {
                        putValues("potassium", doubleVal(potassiumGrams))
                    }
                    if (proteinGrams > 0) {
                        putValues("protein", doubleVal(proteinGrams))
                    }
                    if (riboflavinGrams > 0) {
                        putValues("riboflavin", doubleVal(riboflavinGrams))
                    }
                    if (saturatedFatGrams > 0) {
                        putValues("saturatedFat", doubleVal(saturatedFatGrams))
                    }
                    if (seleniumGrams > 0) {
                        putValues("selenium", doubleVal(seleniumGrams))
                    }
                    if (sodiumGrams > 0) {
                        putValues("sodium", doubleVal(sodiumGrams))
                    }
                    if (sugarGrams > 0) {
                        putValues("sugar", doubleVal(sugarGrams))
                    }
                    if (thiaminGrams > 0) {
                        putValues("thiamin", doubleVal(thiaminGrams))
                    }
                    if (totalCarbohydrateGrams > 0) {
                        putValues("totalCarbohydrate", doubleVal(totalCarbohydrateGrams))
                    }
                    if (totalFatGrams > 0) {
                        putValues("totalFat", doubleVal(totalFatGrams))
                    }
                    if (transFatGrams > 0) {
                        putValues("transFat", doubleVal(transFatGrams))
                    }
                    if (unsaturatedFatGrams > 0) {
                        putValues("unsaturatedFat", doubleVal(unsaturatedFatGrams))
                    }
                    if (vitaminAGrams > 0) {
                        putValues("vitaminA", doubleVal(vitaminAGrams))
                    }
                    if (vitaminB12Grams > 0) {
                        putValues("vitaminB12", doubleVal(vitaminB12Grams))
                    }
                    if (vitaminB6Grams > 0) {
                        putValues("vitaminB6", doubleVal(vitaminB6Grams))
                    }
                    if (vitaminCGrams > 0) {
                        putValues("vitaminC", doubleVal(vitaminCGrams))
                    }
                    if (vitaminDGrams > 0) {
                        putValues("vitaminD", doubleVal(vitaminDGrams))
                    }
                    if (vitaminEGrams > 0) {
                        putValues("vitaminE", doubleVal(vitaminEGrams))
                    }
                    if (vitaminKGrams > 0) {
                        putValues("vitaminK", doubleVal(vitaminKGrams))
                    }
                    if (zincGrams > 0) {
                        putValues("zinc", doubleVal(zincGrams))
                    }
                    mealType?.let { putValues("mealType", enumVal(it)) }
                    name?.let { putValues("name", stringVal(it)) }
                }
                .build()
        is Repetitions ->
            intervalProto()
                .setDataType(protoDataType("Repetitions"))
                .apply {
                    putValues("count", longVal(count))
                    putValues("type", enumVal(type))
                }
                .build()
        is SleepSession ->
            intervalProto()
                .setDataType(protoDataType("SleepSession"))
                .apply {
                    title?.let { putValues("title", stringVal(it)) }
                    notes?.let { putValues("notes", stringVal(it)) }
                }
                .build()
        is SleepStage ->
            intervalProto()
                .setDataType(protoDataType("SleepStage"))
                .apply { putValues("stage", enumVal(stage)) }
                .build()
        is Steps ->
            intervalProto()
                .setDataType(protoDataType("Steps"))
                .apply { putValues("count", longVal(count)) }
                .build()
        is SwimmingStrokes ->
            intervalProto()
                .setDataType(protoDataType("SwimmingStrokes"))
                .apply {
                    if (count > 0) {
                        putValues("count", longVal(count))
                    }
                    putValues("type", enumVal(type))
                }
                .build()
        is TotalCaloriesBurned ->
            intervalProto()
                .setDataType(protoDataType("TotalCaloriesBurned"))
                .apply { putValues("energy", doubleVal(energyKcal)) }
                .build()
        is TotalEnergyBurned ->
            intervalProto()
                .setDataType(protoDataType("TotalEnergyBurned"))
                .apply { putValues("energy", doubleVal(energyKcal)) }
                .build()
        is WheelchairPushes ->
            intervalProto()
                .setDataType(protoDataType("WheelchairPushes"))
                .apply { putValues("count", longVal(count)) }
                .build()
        else -> throw RuntimeException("Unsupported yet!")
    }

private fun <T : Any> SeriesRecord<T>.toProto(
    dataTypeName: String,
    getSeriesValue: (sample: T) -> DataProto.SeriesValue,
): DataProto.DataPoint =
    intervalProto()
        .setDataType(protoDataType(dataTypeName = dataTypeName))
        .apply {
            for (sample in samples) {
                addSeriesValues(getSeriesValue(sample))
            }
        }
        .build()