DataPoint.kt

/*
 * Copyright (C) 2021 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.services.client.data

import android.os.Bundle
import android.os.Parcelable
import androidx.health.services.client.proto.DataProto
import java.time.Duration
import java.time.Instant

/**
 * A data point containing a [value] of type [dataType] from either a single point in time:
 * [DataType.TimeType.SAMPLE], or a range in time: [DataType.TimeType.INTERVAL].
 */
@Suppress("DataClassPrivateConstructor", "ParcelCreator")
public class DataPoint
internal constructor(
    public val dataType: DataType,
    public val value: Value,

    /**
     * Elapsed start time of this [DataPoint].
     *
     * This represents the time at which this [DataPoint] originated, as a [Duration] since boot
     * time. This is not exposed as a timestamp as the clock may drift between when the data is
     * generated and when it is read out. Use [getStartInstant] to get the start time of this
     * [DataPoint] as an [Instant].
     */
    public val startDurationFromBoot: Duration,

    /**
     * Elapsed end time of this [DataPoint].
     *
     * This represents the time at which this [DataPoint] ends, as a [Duration] since boot time.
     * This is not exposed as a timestamp as the clock may drift between when the data is generated
     * and when it is read out. Use [getStartInstant] to get the start time of this [DataPoint] as
     * an [Instant].
     *
     * For instantaneous data points, this is equal to [startDurationFromBoot].
     */
    public val endDurationFromBoot: Duration = startDurationFromBoot,

    /** Returns any provided metadata of this [DataPoint]. */
    public val metadata: Bundle = Bundle(),

    /**
     * Returns the accuracy of this [DataPoint].
     *
     * The specific [DataPointAccuracy] implementation this refers to depends on the [DataType] of
     * the data point. For example, accuracy of [DataType.LOCATION] data points is represented by
     * [LocationAccuracy]. If there is no associated [DataPointAccuracy] for the [DataType], this
     * will return `null`.
     */
    public val accuracy: DataPointAccuracy? = null,
) : ProtoParcelable<DataProto.DataPoint>() {

    internal constructor(
        proto: DataProto.DataPoint
    ) : this(
        DataType(proto.dataType),
        Value(proto.value),
        Duration.ofMillis(proto.startDurationFromBootMs),
        Duration.ofMillis(proto.endDurationFromBootMs),
        BundlesUtil.fromProto(proto.metaData),
        if (proto.hasAccuracy()) DataPointAccuracy.fromProto(proto.accuracy) else null
    )

    /** @hide */
    override val proto: DataProto.DataPoint by lazy {
        val builder =
            DataProto.DataPoint.newBuilder()
                .setDataType(dataType.proto)
                .setValue(value.proto)
                .setStartDurationFromBootMs(startDurationFromBoot.toMillis())
                .setEndDurationFromBootMs(endDurationFromBoot.toMillis())
                .setMetaData(BundlesUtil.toProto(metadata))

        accuracy?.let { builder.setAccuracy(it.proto) }

        builder.build()
    }

    init {
        require(dataType.format == value.format) {
            "DataType and Value format must match, but got ${dataType.format} and ${value.format}"
        }
    }

    /**
     * Returns the start [Instant] of this [DataPoint], knowing the time at which the system booted.
     *
     * @param bootInstant the [Instant] at which the system booted, this can be computed by
     * `Instant.ofEpochMilli(System.currentTimeMillis() - SystemClock.elapsedRealtime()) `
     */
    public fun getStartInstant(bootInstant: Instant): Instant {
        return bootInstant.plus(startDurationFromBoot)
    }

    /**
     * Returns the end [Instant] of this [DataPoint], knowing the time at which the system booted.
     *
     * @param bootInstant the [Instant] at which the system booted, this can be computed by
     * `Instant.ofEpochMilli(System.currentTimeMillis() - SystemClock.elapsedRealtime())`
     */
    public fun getEndInstant(bootInstant: Instant): Instant {
        return bootInstant.plus(endDurationFromBoot)
    }

    override fun toString(): String =
        "DataPoint(" +
            "dataType=$dataType, " +
            "value=$value, " +
            "startDurationFromBoot=$startDurationFromBoot, " +
            "endDurationFromBoot=$endDurationFromBoot, " +
            "accuracy=$accuracy)"

    public companion object {
        @JvmField
        public val CREATOR: Parcelable.Creator<DataPoint> = newCreator {
            val proto = DataProto.DataPoint.parseFrom(it)
            DataPoint(proto)
        }

        /**
         * Returns a [DataPoint] representing the [value] of type [dataType] from
         * [startDurationFromBoot] to [endDurationFromBoot].
         *
         * @throws IllegalArgumentException if the [DataType.TimeType] of the associated [DataType]
         * is not [DataType.TimeType.INTERVAL], or if data is malformed
         */
        @JvmStatic
        @JvmOverloads
        public fun createInterval(
            dataType: DataType,
            value: Value,
            startDurationFromBoot: Duration,
            endDurationFromBoot: Duration,
            metadata: Bundle = Bundle(),
            accuracy: DataPointAccuracy? = null
        ): DataPoint {
            require(DataType.TimeType.INTERVAL == dataType.timeType) {
                "DataType $dataType must be of interval type to be created with an interval"
            }

            require(endDurationFromBoot >= startDurationFromBoot) {
                "End timestamp mustn't be earlier than start timestamp, but got " +
                    "$startDurationFromBoot and $endDurationFromBoot"
            }

            return DataPoint(
                dataType,
                value,
                startDurationFromBoot,
                endDurationFromBoot,
                metadata,
                accuracy
            )
        }

        /**
         * Returns a [DataPoint] representing the [value] of type [dataType] at [durationFromBoot].
         *
         * @throws IllegalArgumentException if the [DataType.TimeType] of the associated [DataType]
         * is not [DataType.TimeType.SAMPLE], or if data is malformed
         */
        @JvmStatic
        @JvmOverloads
        public fun createSample(
            dataType: DataType,
            value: Value,
            durationFromBoot: Duration,
            metadata: Bundle = Bundle(),
            accuracy: DataPointAccuracy? = null
        ): DataPoint {
            require(DataType.TimeType.SAMPLE == dataType.timeType) {
                "DataType $dataType must be of sample type to be created with a single timestamp"
            }

            return DataPoint(
                dataType,
                value,
                durationFromBoot,
                endDurationFromBoot = durationFromBoot,
                metadata,
                accuracy
            )
        }
    }
}