[go: nahoru, domu]

blob: bf9b12f61ea98ab4f98156688185d9f90255954c [file] [log] [blame]
/*
* 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 androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope
import androidx.health.services.client.data.ExerciseEndReason.Companion.toProto
import androidx.health.services.client.data.ExerciseUpdate.ActiveDurationCheckpoint
import androidx.health.services.client.proto.DataProto
import androidx.health.services.client.proto.DataProto.AchievedExerciseGoal
import androidx.health.services.client.proto.DataProto.ExerciseUpdate.LatestMetricsEntry as LatestMetricsEntryProto
import java.time.Duration
import java.time.Instant
/** Contains the latest updated state and metrics for the current exercise. */
@Suppress("ParcelCreator")
public class ExerciseUpdate internal constructor(
/** Returns the list of the latest [DataPoint]s. */
public val latestMetrics: DataPointContainer,
/**
* Returns the latest [ExerciseGoalType.ONE_TIME_GOAL] [ExerciseGoal]s that have been achieved.
* [ExerciseGoalType.MILESTONE] [ExerciseGoal]s will be returned via
* [latestMilestoneMarkerSummaries].
*/
public val latestAchievedGoals: Set<ExerciseGoal<out Number>>,
/** Returns the latest [MilestoneMarkerSummary]s. */
public val latestMilestoneMarkerSummaries: Set<MilestoneMarkerSummary>,
/**
* Returns the [ExerciseStateInfo] containing the current [ExerciseState] and
* [ExerciseEndReason], if applicable.
*/
public val exerciseStateInfo: ExerciseStateInfo,
/**
* Returns the [ExerciseConfig] used by the exercise when the [ExerciseUpdate] was dispatched
* and returns `null` if the exercise is in prepare phase and hasn't been started yet.
*/
public val exerciseConfig: ExerciseConfig? = null,
/**
* Returns the [ActiveDurationCheckpoint] which can be used to determine the active duration of
* the exercise in a way that is consistent with Health Services. Clients can anchor their
* application timers against this to ensure their view of the active duration matches the view
* of Health Services.
*/
public val activeDurationCheckpoint: ActiveDurationCheckpoint? = null,
/** The duration since boot when this ExerciseUpdate was created. */
private val updateDurationFromBoot: Duration? = null,
/**
* Returns the time at which the exercise was started or `null` if the exercise is in prepare
* phase and hasn't started yet.
*/
public val startTime: Instant? = null,
internal val activeDurationLegacy: Duration,
) {
@RestrictTo(Scope.LIBRARY)
public constructor(
proto: DataProto.ExerciseUpdate
) : this(
exerciseUpdateProtoToDataPointContainer(proto),
proto.latestAchievedGoalsList.map { ExerciseGoal.fromProto(it.exerciseGoal) }.toSet(),
proto.mileStoneMarkerSummariesList.map { MilestoneMarkerSummary(it) }.toSet(),
ExerciseStateInfo(
ExerciseState.fromProto(proto.state)
?: throw IllegalArgumentException("Invalid ExerciseState: ${proto.state}"),
ExerciseEndReason.fromProto(proto.exerciseEndReason)
),
if (proto.hasExerciseConfig()) ExerciseConfig(proto.exerciseConfig) else null,
if (proto.hasActiveDurationCheckpoint()) {
ActiveDurationCheckpoint.fromProto(proto.activeDurationCheckpoint)
} else {
null
},
if (proto.hasUpdateDurationFromBootMs()) {
Duration.ofMillis(proto.updateDurationFromBootMs)
} else {
null
},
if (proto.hasStartTimeEpochMs()) Instant.ofEpochMilli(proto.startTimeEpochMs) else null,
Duration.ofMillis(proto.activeDurationMs),
)
/**
* This records the last time the exercise transitioned from an active to an inactive state or
* from an inactive to an active state, where inactive states match those found in
* [ExerciseState.isPaused] or [ExerciseState.isEnded]. This can be used to calculate the
* exercise active duration in a way that is consistent with Health Service's view of the
* exercise.
*
* If the exercise is currently inactive, the exercise’s active duration will match
* [activeDuration] below. If the exercise is active, the active duration can be calculated by
* `activeDuration + (now() - time)`.
*/
public class ActiveDurationCheckpoint(
/**
* Returns the time at which the exercise last transitioned between the active or inactive
* states.
*/
public val time: Instant,
/**
* Returns the active duration of the exercise at the time it last transitioned to "active",
* or the duration when it transitioned to inactive if it's currently paused or stopped.
*/
public val activeDuration: Duration,
) {
@RestrictTo(Scope.LIBRARY)
internal fun toProto(): DataProto.ExerciseUpdate.ActiveDurationCheckpoint =
DataProto.ExerciseUpdate.ActiveDurationCheckpoint.newBuilder()
.setTimeEpochMs(time.toEpochMilli())
.setActiveDurationMs(activeDuration.toMillis())
.build()
override fun toString(): String =
"ActiveDurationCheckpoint(time=$time, activeDuration=$activeDuration)"
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ActiveDurationCheckpoint
if (time != other.time) return false
if (activeDuration != other.activeDuration) return false
return true
}
override fun hashCode(): Int {
var result = time.hashCode()
result = 31 * result + activeDuration.hashCode()
return result
}
internal companion object {
@RestrictTo(Scope.LIBRARY)
internal fun fromProto(
proto: DataProto.ExerciseUpdate.ActiveDurationCheckpoint
): ActiveDurationCheckpoint? =
ActiveDurationCheckpoint(
Instant.ofEpochMilli(proto.timeEpochMs),
Duration.ofMillis(proto.activeDurationMs)
)
}
}
internal val proto: DataProto.ExerciseUpdate = getExerciseUpdateProto()
private fun getExerciseUpdateProto(): DataProto.ExerciseUpdate {
val builder =
DataProto.ExerciseUpdate.newBuilder()
.setState(exerciseStateInfo.state.toProto())
.setActiveDurationMs(activeDurationLegacy.toMillis())
.addAllLatestMetrics(
latestMetrics.sampleDataPoints
.groupBy { it.dataType }
.map {
LatestMetricsEntryProto.newBuilder()
.setDataType(it.key.proto)
.addAllDataPoints(it.value.map(SampleDataPoint<*>::proto))
.build()
}
// If we don't sort, equals() may not work.
.sortedBy { entry -> entry.dataType.name }
)
.addAllLatestMetrics(
latestMetrics.intervalDataPoints
.groupBy { it.dataType }
.map {
LatestMetricsEntryProto.newBuilder()
.setDataType(it.key.proto)
.addAllDataPoints(it.value.map(IntervalDataPoint<*>::proto))
.build()
}
// If we don't sort, equals() may not work.
.sortedBy { entry -> entry.dataType.name }
)
.addAllLatestAggregateMetrics(
latestMetrics.statisticalDataPoints
.map { it.proto }
// If we don't sort, equals() may not work.
.sortedBy { entry -> entry.statisticalDataPoint.dataType.name }
).addAllLatestAggregateMetrics(latestMetrics.cumulativeDataPoints
.map { it.proto }
// If we don't sort, equals() may not work.
.sortedBy { entry -> entry.cumulativeDataPoint.dataType.name })
.addAllLatestAchievedGoals(latestAchievedGoals.map {
AchievedExerciseGoal.newBuilder().setExerciseGoal(it.proto).build()
}).addAllMileStoneMarkerSummaries(latestMilestoneMarkerSummaries.map { it.proto })
.setExerciseEndReason((exerciseStateInfo.endReason).toProto())
startTime?.let { builder.setStartTimeEpochMs(startTime.toEpochMilli()) }
updateDurationFromBoot?.let {
builder.setUpdateDurationFromBootMs(updateDurationFromBoot.toMillis())
}
exerciseConfig?.let { builder.setExerciseConfig(exerciseConfig.toProto()) }
activeDurationCheckpoint?.let {
builder.setActiveDurationCheckpoint(activeDurationCheckpoint.toProto())
}
return builder.build()
}
/**
* Returns the duration since boot when this ExerciseUpdate was created.
*
* @throws IllegalStateException if this [ExerciseUpdate] does not contain a valid
* `updateDurationFromBoot` which may happen if the Health Services app is out of date
*/
public fun getUpdateDurationFromBoot(): Duration =
updateDurationFromBoot
?: error(
"updateDurationFromBoot unavailable; is the Health Services APK out of date?"
)
/**
* Returns the ActiveDuration of the exercise at the time of the provided [IntervalDataPoint].
* The provided [IntervalDataPoint] should be present in this [ExerciseUpdate].
*
* @throws IllegalArgumentException if [dataPoint] is not present in this [ExerciseUpdate]
* @throws IllegalStateException if this [ExerciseUpdate] does not contain a valid
* `updateDurationFromBoot` which may happen if the Health Services app is out of date
*/
public fun getActiveDurationAtDataPoint(dataPoint: IntervalDataPoint<*>): Duration =
getActiveDurationAtDataPoint(dataPoint, dataPoint.endDurationFromBoot)
/**
* Returns the ActiveDuration of the exercise at the time of the provided [SampleDataPoint].
* The provided [SampleDataPoint] should be present in this [ExerciseUpdate].
*
* @throws IllegalArgumentException if [dataPoint] is not present in this [ExerciseUpdate]
*/
public fun getActiveDurationAtDataPoint(dataPoint: SampleDataPoint<*>): Duration =
getActiveDurationAtDataPoint(dataPoint, dataPoint.timeDurationFromBoot)
private fun getActiveDurationAtDataPoint(
dataPoint: DataPoint<*>,
durationFromBoot: Duration
): Duration {
val dataPointList = latestMetrics.dataPoints[dataPoint.dataType]
if (dataPointList?.indexOf(dataPoint) == -1) {
throw IllegalArgumentException("dataPoint not found in ExerciseUpdate")
}
// If activeDurationCheckpoint is null, user has not started their activity yet. Default to
// zero.
if (activeDurationCheckpoint == null) {
return Duration.ZERO
}
// If we are paused then the last active time applies to all updates.
if (exerciseStateInfo.state == ExerciseState.USER_PAUSED ||
exerciseStateInfo.state == ExerciseState.AUTO_PAUSED
) {
return activeDurationCheckpoint.activeDuration
}
// Active duration applies to when this update was generated so calculate for the given time
// by working backwards.
// First find time since this point was generated.
val durationSinceProvidedTime = getUpdateDurationFromBoot().minus(durationFromBoot)
return activeDurationLegacy.minus(durationSinceProvidedTime)
}
override fun toString(): String =
"ExerciseUpdate(" +
"state=$exerciseStateInfo.state, " +
"startTime=$startTime, " +
"updateDurationFromBoot=$updateDurationFromBoot, " +
"latestMetrics=$latestMetrics, " +
"latestAchievedGoals=$latestAchievedGoals, " +
"latestMilestoneMarkerSummaries=$latestMilestoneMarkerSummaries, " +
"exerciseConfig=$exerciseConfig, " +
"activeDurationCheckpoint=$activeDurationCheckpoint, " +
"exerciseEndReason=${exerciseStateInfo.endReason}" +
")"
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ExerciseUpdate) return false
if (startTime != other.startTime) return false
if (latestMetrics != other.latestMetrics) return false
if (latestAchievedGoals != other.latestAchievedGoals) return false
if (latestMilestoneMarkerSummaries != other.latestMilestoneMarkerSummaries) return false
if (exerciseConfig != other.exerciseConfig) return false
if (activeDurationCheckpoint != other.activeDurationCheckpoint) return false
if (exerciseStateInfo != other.exerciseStateInfo) return false
if (updateDurationFromBoot != other.updateDurationFromBoot) return false
return true
}
override fun hashCode(): Int {
var result = startTime?.hashCode() ?: 0
result = 31 * result + latestMetrics.hashCode()
result = 31 * result + latestAchievedGoals.hashCode()
result = 31 * result + latestMilestoneMarkerSummaries.hashCode()
result = 31 * result + (exerciseConfig?.hashCode() ?: 0)
result = 31 * result + (activeDurationCheckpoint?.hashCode() ?: 0)
result = 31 * result + exerciseStateInfo.hashCode()
result = 31 * result + (updateDurationFromBoot?.hashCode() ?: 0)
return result
}
public companion object {
internal fun exerciseUpdateProtoToDataPointContainer(
proto: DataProto.ExerciseUpdate
): DataPointContainer {
val dataPoints = mutableListOf<DataPoint<*>>()
proto.latestMetricsList
.flatMap { it.dataPointsList }
.forEach {
dataPoints += DataPoint.fromProto(it)
}
proto.latestAggregateMetricsList
.forEach {
dataPoints += DataPoint.fromProto(it)
}
return DataPointContainer(dataPoints)
}
}
}