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.
 */
@file:RestrictTo(RestrictTo.Scope.LIBRARY)

package androidx.health.connect.client.impl.converters.records

import androidx.annotation.RestrictTo
import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
import androidx.health.connect.client.records.BasalBodyTemperatureRecord
import androidx.health.connect.client.records.BasalMetabolicRateRecord
import androidx.health.connect.client.records.BloodGlucoseRecord
import androidx.health.connect.client.records.BloodPressureRecord
import androidx.health.connect.client.records.BodyFatRecord
import androidx.health.connect.client.records.BodyTemperatureRecord
import androidx.health.connect.client.records.BodyWaterMassRecord
import androidx.health.connect.client.records.BoneMassRecord
import androidx.health.connect.client.records.CervicalMucusRecord
import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
import androidx.health.connect.client.records.DistanceRecord
import androidx.health.connect.client.records.ElevationGainedRecord
import androidx.health.connect.client.records.ExerciseEventRecord
import androidx.health.connect.client.records.ExerciseLapRecord
import androidx.health.connect.client.records.ExerciseRepetitionsRecord
import androidx.health.connect.client.records.ExerciseSessionRecord
import androidx.health.connect.client.records.FloorsClimbedRecord
import androidx.health.connect.client.records.HeartRateRecord
import androidx.health.connect.client.records.HeartRateVariabilityDifferentialIndexRecord
import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
import androidx.health.connect.client.records.HeartRateVariabilitySRecord
import androidx.health.connect.client.records.HeartRateVariabilitySd2Record
import androidx.health.connect.client.records.HeartRateVariabilitySdannRecord
import androidx.health.connect.client.records.HeartRateVariabilitySdnnIndexRecord
import androidx.health.connect.client.records.HeartRateVariabilitySdnnRecord
import androidx.health.connect.client.records.HeartRateVariabilitySdsdRecord
import androidx.health.connect.client.records.HeartRateVariabilityTinnRecord
import androidx.health.connect.client.records.HeightRecord
import androidx.health.connect.client.records.HipCircumferenceRecord
import androidx.health.connect.client.records.HydrationRecord
import androidx.health.connect.client.records.LeanBodyMassRecord
import androidx.health.connect.client.records.MenstruationFlowRecord
import androidx.health.connect.client.records.NutritionRecord
import androidx.health.connect.client.records.OvulationTestRecord
import androidx.health.connect.client.records.OxygenSaturationRecord
import androidx.health.connect.client.records.PowerRecord
import androidx.health.connect.client.records.Record
import androidx.health.connect.client.records.RespiratoryRateRecord
import androidx.health.connect.client.records.RestingHeartRateRecord
import androidx.health.connect.client.records.SeriesRecord
import androidx.health.connect.client.records.SexualActivityRecord
import androidx.health.connect.client.records.SleepSessionRecord
import androidx.health.connect.client.records.SleepStageRecord
import androidx.health.connect.client.records.SpeedRecord
import androidx.health.connect.client.records.StepsCadenceRecord
import androidx.health.connect.client.records.StepsRecord
import androidx.health.connect.client.records.SwimmingStrokesRecord
import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
import androidx.health.connect.client.records.Vo2MaxRecord
import androidx.health.connect.client.records.WaistCircumferenceRecord
import androidx.health.connect.client.records.WeightRecord
import androidx.health.connect.client.records.WheelchairPushesRecord
import androidx.health.platform.client.proto.DataProto

/** Converts public API object into internal proto for ipc. */
fun Record.toProto(): DataProto.DataPoint =
    when (this) {
        is BasalBodyTemperatureRecord ->
            instantaneousProto()
                .setDataType(protoDataType("BasalBodyTemperature"))
                .apply {
                    putValues("temperature", doubleVal(temperature.inCelsius))
                    measurementLocation?.let { putValues("measurementLocation", enumVal(it)) }
                }
                .build()
        is BasalMetabolicRateRecord ->
            instantaneousProto()
                .setDataType(protoDataType("BasalMetabolicRate"))
                .apply { putValues("bmr", doubleVal(basalMetabolicRate.inKilocaloriesPerDay)) }
                .build()
        is BloodGlucoseRecord ->
            instantaneousProto()
                .setDataType(protoDataType("BloodGlucose"))
                .apply {
                    putValues("level", doubleVal(level.inMillimolesPerLiter))
                    specimenSource?.let { putValues("specimenSource", enumVal(it)) }
                    mealType?.let { putValues("mealType", enumVal(it)) }
                    relationToMeal?.let { putValues("relationToMeal", enumVal(it)) }
                }
                .build()
        is BloodPressureRecord ->
            instantaneousProto()
                .setDataType(protoDataType("BloodPressure"))
                .apply {
                    putValues("systolic", doubleVal(systolic.inMillimetersOfMercury))
                    putValues("diastolic", doubleVal(diastolic.inMillimetersOfMercury))
                    bodyPosition?.let { putValues("bodyPosition", enumVal(it)) }
                    measurementLocation?.let { putValues("measurementLocation", enumVal(it)) }
                }
                .build()
        is BodyFatRecord ->
            instantaneousProto()
                .setDataType(protoDataType("BodyFat"))
                .apply { putValues("percentage", doubleVal(percentage.value)) }
                .build()
        is BodyTemperatureRecord ->
            instantaneousProto()
                .setDataType(protoDataType("BodyTemperature"))
                .apply {
                    putValues("temperature", doubleVal(temperature.inCelsius))
                    measurementLocation?.let { putValues("measurementLocation", enumVal(it)) }
                }
                .build()
        is BodyWaterMassRecord ->
            instantaneousProto()
                .setDataType(protoDataType("BodyWaterMass"))
                .apply { putValues("mass", doubleVal(mass.inKilograms)) }
                .build()
        is BoneMassRecord ->
            instantaneousProto()
                .setDataType(protoDataType("BoneMass"))
                .apply { putValues("mass", doubleVal(mass.inKilograms)) }
                .build()
        is CervicalMucusRecord ->
            instantaneousProto()
                .setDataType(protoDataType("CervicalMucus"))
                .apply {
                    appearance?.let { putValues("texture", enumVal(it)) }
                    sensation?.let { putValues("amount", enumVal(it)) }
                }
                .build()
        is CyclingPedalingCadenceRecord ->
            toProto(dataTypeName = "CyclingPedalingCadenceSeries") { sample ->
                DataProto.SeriesValue.newBuilder()
                    .putValues("rpm", doubleVal(sample.revolutionsPerMinute))
                    .setInstantTimeMillis(sample.time.toEpochMilli())
                    .build()
            }
        is HeartRateRecord ->
            toProto(dataTypeName = "HeartRateSeries") { sample ->
                DataProto.SeriesValue.newBuilder()
                    .putValues("bpm", longVal(sample.beatsPerMinute))
                    .setInstantTimeMillis(sample.time.toEpochMilli())
                    .build()
            }
        is HeightRecord ->
            instantaneousProto()
                .setDataType(protoDataType("Height"))
                .apply { putValues("height", doubleVal(height.inMeters)) }
                .build()
        is HipCircumferenceRecord ->
            instantaneousProto()
                .setDataType(protoDataType("HipCircumference"))
                .apply { putValues("circumference", doubleVal(circumference.inMeters)) }
                .build()
        is HeartRateVariabilityDifferentialIndexRecord ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilityDifferentialIndex"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilityRmssdRecord ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilityRmssd"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilitySRecord ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilityS"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilitySd2Record ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilitySd2"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilitySdannRecord ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilitySdann"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilitySdnnIndexRecord ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilitySdnnIndex"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilitySdnnRecord ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilitySdnn"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilitySdsdRecord ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilitySdsd"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is HeartRateVariabilityTinnRecord ->
            instantaneousProto()
                .setDataType(protoDataType("HeartRateVariabilityTinn"))
                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                .build()
        is LeanBodyMassRecord ->
            instantaneousProto()
                .setDataType(protoDataType("LeanBodyMass"))
                .apply { putValues("mass", doubleVal(mass.inKilograms)) }
                .build()
        is MenstruationFlowRecord ->
            instantaneousProto()
                .setDataType(protoDataType("Menstruation"))
                .apply { flow?.let { putValues("flow", enumVal(it)) } }
                .build()
        is OvulationTestRecord ->
            instantaneousProto()
                .setDataType(protoDataType("OvulationTest"))
                .apply { putValues("result", enumVal(result)) }
                .build()
        is OxygenSaturationRecord ->
            instantaneousProto()
                .setDataType(protoDataType("OxygenSaturation"))
                .apply { putValues("percentage", doubleVal(percentage.value)) }
                .build()
        is PowerRecord ->
            toProto(dataTypeName = "PowerSeries") { sample ->
                DataProto.SeriesValue.newBuilder()
                    .putValues("power", doubleVal(sample.power.inWatts))
                    .setInstantTimeMillis(sample.time.toEpochMilli())
                    .build()
            }
        is RespiratoryRateRecord ->
            instantaneousProto()
                .setDataType(protoDataType("RespiratoryRate"))
                .apply { putValues("rate", doubleVal(rate)) }
                .build()
        is RestingHeartRateRecord ->
            instantaneousProto()
                .setDataType(protoDataType("RestingHeartRate"))
                .apply { putValues("bpm", longVal(beatsPerMinute)) }
                .build()
        is SexualActivityRecord ->
            instantaneousProto()
                .setDataType(protoDataType("SexualActivity"))
                .apply { protectionUsed?.let { putValues("protectionUsed", enumVal(it)) } }
                .build()
        is SpeedRecord ->
            toProto(dataTypeName = "SpeedSeries") { sample ->
                DataProto.SeriesValue.newBuilder()
                    .putValues("speed", doubleVal(sample.speed.inMetersPerSecond))
                    .setInstantTimeMillis(sample.time.toEpochMilli())
                    .build()
            }
        is StepsCadenceRecord ->
            toProto(dataTypeName = "StepsCadenceSeries") { sample ->
                DataProto.SeriesValue.newBuilder()
                    .putValues("rate", doubleVal(sample.rate))
                    .setInstantTimeMillis(sample.time.toEpochMilli())
                    .build()
            }
        is Vo2MaxRecord ->
            instantaneousProto()
                .setDataType(protoDataType("Vo2Max"))
                .apply {
                    putValues("vo2", doubleVal(vo2MillilitersPerMinuteKilogram))
                    measurementMethod?.let { putValues("measurementMethod", enumVal(it)) }
                }
                .build()
        is WaistCircumferenceRecord ->
            instantaneousProto()
                .setDataType(protoDataType("WaistCircumference"))
                .apply { putValues("circumference", doubleVal(circumference.inMeters)) }
                .build()
        is WeightRecord ->
            instantaneousProto()
                .setDataType(protoDataType("Weight"))
                .apply { putValues("weight", doubleVal(weight.inKilograms)) }
                .build()
        is ActiveCaloriesBurnedRecord ->
            intervalProto()
                .setDataType(protoDataType("ActiveCaloriesBurned"))
                .apply { putValues("energy", doubleVal(energy.inKilocalories)) }
                .build()
        is ExerciseEventRecord ->
            intervalProto()
                .setDataType(protoDataType("ActivityEvent"))
                .apply { putValues("eventType", enumVal(eventType)) }
                .build()
        is ExerciseLapRecord ->
            intervalProto()
                .setDataType(protoDataType("ActivityLap"))
                .apply {
                    if (length != null) {
                        putValues("length", doubleVal(length.inMeters))
                    }
                }
                .build()
        is ExerciseSessionRecord ->
            intervalProto()
                .setDataType(protoDataType("ActivitySession"))
                .apply {
                    putValues("activityType", enumVal(exerciseType))
                    title?.let { putValues("title", stringVal(it)) }
                    notes?.let { putValues("notes", stringVal(it)) }
                }
                .build()
        is DistanceRecord ->
            intervalProto()
                .setDataType(protoDataType("Distance"))
                .apply { putValues("distance", doubleVal(distance.inMeters)) }
                .build()
        is ElevationGainedRecord ->
            intervalProto()
                .setDataType(protoDataType("ElevationGained"))
                .apply { putValues("elevation", doubleVal(elevation.inMeters)) }
                .build()
        is FloorsClimbedRecord ->
            intervalProto()
                .setDataType(protoDataType("FloorsClimbed"))
                .apply { putValues("floors", doubleVal(floors)) }
                .build()
        is HydrationRecord ->
            intervalProto()
                .setDataType(protoDataType("Hydration"))
                .apply { putValues("volume", doubleVal(volume.inLiters)) }
                .build()
        is NutritionRecord ->
            intervalProto()
                .setDataType(protoDataType("Nutrition"))
                .apply {
                    if (biotin != null) {
                        putValues("biotin", doubleVal(biotin.inGrams))
                    }
                    if (caffeine != null) {
                        putValues("caffeine", doubleVal(caffeine.inGrams))
                    }
                    if (calcium != null) {
                        putValues("calcium", doubleVal(calcium.inGrams))
                    }
                    if (energy != null) {
                        putValues("calories", doubleVal(energy.inKilocalories))
                    }
                    if (energyFromFat != null) {
                        putValues("caloriesFromFat", doubleVal(energyFromFat.inKilocalories))
                    }
                    if (chloride != null) {
                        putValues("chloride", doubleVal(chloride.inGrams))
                    }
                    if (cholesterol != null) {
                        putValues("cholesterol", doubleVal(cholesterol.inGrams))
                    }
                    if (chromium != null) {
                        putValues("chromium", doubleVal(chromium.inGrams))
                    }
                    if (copper != null) {
                        putValues("copper", doubleVal(copper.inGrams))
                    }
                    if (dietaryFiber != null) {
                        putValues("dietaryFiber", doubleVal(dietaryFiber.inGrams))
                    }
                    if (folate != null) {
                        putValues("folate", doubleVal(folate.inGrams))
                    }
                    if (folicAcid != null) {
                        putValues("folicAcid", doubleVal(folicAcid.inGrams))
                    }
                    if (iodine != null) {
                        putValues("iodine", doubleVal(iodine.inGrams))
                    }
                    if (iron != null) {
                        putValues("iron", doubleVal(iron.inGrams))
                    }
                    if (magnesium != null) {
                        putValues("magnesium", doubleVal(magnesium.inGrams))
                    }
                    if (manganese != null) {
                        putValues("manganese", doubleVal(manganese.inGrams))
                    }
                    if (molybdenum != null) {
                        putValues("molybdenum", doubleVal(molybdenum.inGrams))
                    }
                    if (monounsaturatedFat != null) {
                        putValues("monounsaturatedFat", doubleVal(monounsaturatedFat.inGrams))
                    }
                    if (niacin != null) {
                        putValues("niacin", doubleVal(niacin.inGrams))
                    }
                    if (pantothenicAcid != null) {
                        putValues("pantothenicAcid", doubleVal(pantothenicAcid.inGrams))
                    }
                    if (phosphorus != null) {
                        putValues("phosphorus", doubleVal(phosphorus.inGrams))
                    }
                    if (polyunsaturatedFat != null) {
                        putValues("polyunsaturatedFat", doubleVal(polyunsaturatedFat.inGrams))
                    }
                    if (potassium != null) {
                        putValues("potassium", doubleVal(potassium.inGrams))
                    }
                    if (protein != null) {
                        putValues("protein", doubleVal(protein.inGrams))
                    }
                    if (riboflavin != null) {
                        putValues("riboflavin", doubleVal(riboflavin.inGrams))
                    }
                    if (saturatedFat != null) {
                        putValues("saturatedFat", doubleVal(saturatedFat.inGrams))
                    }
                    if (selenium != null) {
                        putValues("selenium", doubleVal(selenium.inGrams))
                    }
                    if (sodium != null) {
                        putValues("sodium", doubleVal(sodium.inGrams))
                    }
                    if (sugar != null) {
                        putValues("sugar", doubleVal(sugar.inGrams))
                    }
                    if (thiamin != null) {
                        putValues("thiamin", doubleVal(thiamin.inGrams))
                    }
                    if (totalCarbohydrate != null) {
                        putValues("totalCarbohydrate", doubleVal(totalCarbohydrate.inGrams))
                    }
                    if (totalFat != null) {
                        putValues("totalFat", doubleVal(totalFat.inGrams))
                    }
                    if (transFat != null) {
                        putValues("transFat", doubleVal(transFat.inGrams))
                    }
                    if (unsaturatedFat != null) {
                        putValues("unsaturatedFat", doubleVal(unsaturatedFat.inGrams))
                    }
                    if (vitaminA != null) {
                        putValues("vitaminA", doubleVal(vitaminA.inGrams))
                    }
                    if (vitaminB12 != null) {
                        putValues("vitaminB12", doubleVal(vitaminB12.inGrams))
                    }
                    if (vitaminB6 != null) {
                        putValues("vitaminB6", doubleVal(vitaminB6.inGrams))
                    }
                    if (vitaminC != null) {
                        putValues("vitaminC", doubleVal(vitaminC.inGrams))
                    }
                    if (vitaminD != null) {
                        putValues("vitaminD", doubleVal(vitaminD.inGrams))
                    }
                    if (vitaminE != null) {
                        putValues("vitaminE", doubleVal(vitaminE.inGrams))
                    }
                    if (vitaminK != null) {
                        putValues("vitaminK", doubleVal(vitaminK.inGrams))
                    }
                    if (zinc != null) {
                        putValues("zinc", doubleVal(zinc.inGrams))
                    }
                    mealType?.let { putValues("mealType", enumVal(it)) }
                    name?.let { putValues("name", stringVal(it)) }
                }
                .build()
        is ExerciseRepetitionsRecord ->
            intervalProto()
                .setDataType(protoDataType("Repetitions"))
                .apply {
                    putValues("count", longVal(count))
                    putValues("type", enumVal(type))
                }
                .build()
        is SleepSessionRecord ->
            intervalProto()
                .setDataType(protoDataType("SleepSession"))
                .apply {
                    title?.let { putValues("title", stringVal(it)) }
                    notes?.let { putValues("notes", stringVal(it)) }
                }
                .build()
        is SleepStageRecord ->
            intervalProto()
                .setDataType(protoDataType("SleepStage"))
                .apply { putValues("stage", enumVal(stage)) }
                .build()
        is StepsRecord ->
            intervalProto()
                .setDataType(protoDataType("Steps"))
                .apply { putValues("count", longVal(count)) }
                .build()
        is SwimmingStrokesRecord ->
            intervalProto()
                .setDataType(protoDataType("SwimmingStrokes"))
                .apply {
                    if (count > 0) {
                        putValues("count", longVal(count))
                    }
                    putValues("type", enumVal(type))
                }
                .build()
        is TotalCaloriesBurnedRecord ->
            intervalProto()
                .setDataType(protoDataType("TotalCaloriesBurned"))
                .apply { putValues("energy", doubleVal(energy.inKilocalories)) }
                .build()
        is WheelchairPushesRecord ->
            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()