blob: 3c0cfef682b123eecc7a5626cbb85d4666a60cec [file] [log] [blame]
/*
* Copyright (C) 2023 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 android.health.connect.datatypes;
import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.health.connect.datatypes.units.Length;
import android.health.connect.datatypes.validation.ValidationUtils;
import android.health.connect.internal.datatypes.ExerciseRouteInternal;
import android.os.Parcel;
import android.os.Parcelable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/** Route of the exercise session. Contains sequence of location points with timestamps. */
public final class ExerciseRoute implements Parcelable {
private final List<Location> mRouteLocations;
@NonNull
public static final Creator<ExerciseRoute> CREATOR =
new Creator<>() {
@Override
public ExerciseRoute createFromParcel(Parcel source) {
int size = source.readInt();
List<Location> locations = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
locations.add(i, Location.CREATOR.createFromParcel(source));
}
return new ExerciseRoute(locations);
}
@Override
public ExerciseRoute[] newArray(int size) {
return new ExerciseRoute[size];
}
};
/**
* Creates {@link ExerciseRoute} instance
*
* @param routeLocations list of locations with timestamps that make up the route
*/
public ExerciseRoute(@NonNull List<Location> routeLocations) {
Objects.requireNonNull(routeLocations);
mRouteLocations = routeLocations;
}
@NonNull
public List<Location> getRouteLocations() {
return mRouteLocations;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ExerciseRoute)) return false;
ExerciseRoute that = (ExerciseRoute) o;
return getRouteLocations().equals(that.getRouteLocations());
}
@Override
public int hashCode() {
return Objects.hash(getRouteLocations());
}
/** @hide */
public ExerciseRouteInternal toRouteInternal() {
List<ExerciseRouteInternal.LocationInternal> routeLocations =
new ArrayList<>(getRouteLocations().size());
for (ExerciseRoute.Location location : getRouteLocations()) {
routeLocations.add(location.toExerciseRouteLocationInternal());
}
return new ExerciseRouteInternal(routeLocations);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mRouteLocations.size());
for (int i = 0; i < mRouteLocations.size(); i++) {
mRouteLocations.get(i).writeToParcel(dest, flags);
}
}
/** Point in the time and space. Used in {@link ExerciseRoute}. */
public static final class Location implements Parcelable {
// Values are used for FloatRange annotation in latitude/longitude getters and constructor.
private static final double MIN_LONGITUDE = -180;
private static final double MAX_LONGITUDE = 180;
private static final double MIN_LATITUDE = -90;
private static final double MAX_LATITUDE = 90;
private final Instant mTime;
private final double mLatitude;
private final double mLongitude;
private final Length mHorizontalAccuracy;
private final Length mVerticalAccuracy;
private final Length mAltitude;
@NonNull
public static final Creator<Location> CREATOR =
new Creator<>() {
@Override
public Location createFromParcel(Parcel source) {
Instant timestamp = Instant.ofEpochMilli(source.readLong());
double lat = source.readDouble();
double lon = source.readDouble();
Builder builder = new Builder(timestamp, lat, lon);
if (source.readBoolean()) {
builder.setHorizontalAccuracy(Length.fromMeters(source.readDouble()));
}
if (source.readBoolean()) {
builder.setVerticalAccuracy(Length.fromMeters(source.readDouble()));
}
if (source.readBoolean()) {
builder.setAltitude(Length.fromMeters(source.readDouble()));
}
return builder.build();
}
@Override
public Location[] newArray(int size) {
return new Location[size];
}
};
/**
* Represents a single location in an exercise route.
*
* @param time The point in time when the measurement was taken.
* @param latitude Latitude of a location represented as a double, in degrees. Valid range:
* from -90 to 90 degrees.
* @param longitude Longitude of a location represented as a double, in degrees. Valid
* range: from -180 to 180 degrees.
* @param horizontalAccuracy The radius of uncertainty for the location, in [Length] unit.
* Must be non-negative value.
* @param verticalAccuracy The validity of the altitude values, and their estimated
* uncertainty, in [Length] unit. Must be non-negative value.
* @param altitude An altitude of a location represented as a float, in [Length] unit above
* sea level.
* @param skipValidation Boolean flag to skip validation of record values.
* @see ExerciseRoute
*/
private Location(
@NonNull Instant time,
@FloatRange(from = MIN_LATITUDE, to = MAX_LATITUDE) double latitude,
@FloatRange(from = MIN_LONGITUDE, to = MAX_LONGITUDE) double longitude,
@Nullable Length horizontalAccuracy,
@Nullable Length verticalAccuracy,
@Nullable Length altitude,
boolean skipValidation) {
Objects.requireNonNull(time);
if (!skipValidation) {
ValidationUtils.requireInRange(latitude, MIN_LATITUDE, MAX_LATITUDE, "Latitude");
ValidationUtils.requireInRange(
longitude, MIN_LONGITUDE, MAX_LONGITUDE, "Longitude");
if (horizontalAccuracy != null) {
ValidationUtils.requireNonNegative(
horizontalAccuracy.getInMeters(), "Horizontal accuracy");
}
if (verticalAccuracy != null && verticalAccuracy.getInMeters() < 0) {
ValidationUtils.requireNonNegative(
verticalAccuracy.getInMeters(), "Vertical accuracy");
}
}
mTime = time;
mLatitude = latitude;
mLongitude = longitude;
mHorizontalAccuracy = horizontalAccuracy;
mVerticalAccuracy = verticalAccuracy;
mAltitude = altitude;
}
/** Returns time when this location has been recorded */
@NonNull
public Instant getTime() {
return mTime;
}
/** Returns longitude of this location */
@FloatRange(from = -180.0, to = 180.0)
public double getLongitude() {
return mLongitude;
}
/** Returns latitude of this location */
@FloatRange(from = -90.0, to = 90.0)
public double getLatitude() {
return mLatitude;
}
/**
* Returns horizontal accuracy of this location time point. Returns null if no horizontal
* accuracy was specified.
*/
@Nullable
public Length getHorizontalAccuracy() {
return mHorizontalAccuracy;
}
/**
* Returns vertical accuracy of this location time point. Returns null if no vertical
* accuracy was specified.
*/
@Nullable
public Length getVerticalAccuracy() {
return mVerticalAccuracy;
}
/**
* Returns altitude of this location time point. Returns null if no altitude was specified.
*/
@Nullable
public Length getAltitude() {
return mAltitude;
}
/** @hide */
public ExerciseRouteInternal.LocationInternal toExerciseRouteLocationInternal() {
ExerciseRouteInternal.LocationInternal locationInternal =
new ExerciseRouteInternal.LocationInternal()
.setTime(getTime().toEpochMilli())
.setLatitude(getLatitude())
.setLongitude(getLongitude());
if (getHorizontalAccuracy() != null) {
locationInternal.setHorizontalAccuracy(getHorizontalAccuracy().getInMeters());
}
if (getVerticalAccuracy() != null) {
locationInternal.setVerticalAccuracy(getVerticalAccuracy().getInMeters());
}
if (getAltitude() != null) {
locationInternal.setAltitude(getAltitude().getInMeters());
}
return locationInternal;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Location)) return false;
Location that = (Location) o;
return Objects.equals(getAltitude(), that.getAltitude())
&& getTime().equals(that.getTime())
&& (getLatitude() == that.getLatitude())
&& (getLongitude() == that.getLongitude())
&& Objects.equals(getHorizontalAccuracy(), that.getHorizontalAccuracy())
&& Objects.equals(getVerticalAccuracy(), that.getVerticalAccuracy());
}
@Override
public int hashCode() {
return Objects.hash(
getTime(),
getLatitude(),
getLongitude(),
getHorizontalAccuracy(),
getVerticalAccuracy(),
getAltitude());
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeLong(mTime.toEpochMilli());
dest.writeDouble(mLatitude);
dest.writeDouble(mLongitude);
dest.writeBoolean(mHorizontalAccuracy != null);
if (mHorizontalAccuracy != null) {
dest.writeDouble(mHorizontalAccuracy.getInMeters());
}
dest.writeBoolean(mVerticalAccuracy != null);
if (mVerticalAccuracy != null) {
dest.writeDouble(mVerticalAccuracy.getInMeters());
}
dest.writeBoolean(mAltitude != null);
if (mAltitude != null) {
dest.writeDouble(mAltitude.getInMeters());
}
}
/** Builder class for {@link Location} */
public static final class Builder {
@NonNull private final Instant mTime;
@FloatRange(from = MIN_LATITUDE, to = MAX_LATITUDE)
private final double mLatitude;
@FloatRange(from = MIN_LONGITUDE, to = MAX_LONGITUDE)
private final double mLongitude;
@Nullable private Length mHorizontalAccuracy;
@Nullable private Length mVerticalAccuracy;
@Nullable private Length mAltitude;
/** Sets time, longitude and latitude to the point. */
public Builder(
@NonNull Instant time,
@FloatRange(from = -90.0, to = 90.0) double latitude,
@FloatRange(from = -180.0, to = 180.0) double longitude) {
Objects.requireNonNull(time);
mTime = time;
mLatitude = latitude;
mLongitude = longitude;
}
/** Sets horizontal accuracy to the point. */
@NonNull
public Builder setHorizontalAccuracy(@NonNull Length horizontalAccuracy) {
Objects.requireNonNull(horizontalAccuracy);
mHorizontalAccuracy = horizontalAccuracy;
return this;
}
/** Sets vertical accuracy to the point. */
@NonNull
public Builder setVerticalAccuracy(@NonNull Length verticalAccuracy) {
Objects.requireNonNull(verticalAccuracy);
mVerticalAccuracy = verticalAccuracy;
return this;
}
/** Sets altitude to the point. */
@NonNull
public Builder setAltitude(@NonNull Length altitude) {
Objects.requireNonNull(altitude);
mAltitude = altitude;
return this;
}
/**
* @return Object of {@link Location} without validating the values.
* @hide
*/
@NonNull
public Location buildWithoutValidation() {
return new Location(
mTime,
mLatitude,
mLongitude,
mHorizontalAccuracy,
mVerticalAccuracy,
mAltitude,
true);
}
/** Builds {@link Location} */
@NonNull
public Location build() {
return new Location(
mTime,
mLatitude,
mLongitude,
mHorizontalAccuracy,
mVerticalAccuracy,
mAltitude,
false);
}
}
}
}