1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.health.connect.datatypes; 18 19 import android.annotation.FloatRange; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.health.connect.datatypes.units.Length; 23 import android.health.connect.datatypes.validation.ValidationUtils; 24 import android.health.connect.internal.datatypes.ExerciseRouteInternal; 25 import android.os.Parcel; 26 import android.os.Parcelable; 27 28 import java.time.Instant; 29 import java.util.ArrayList; 30 import java.util.List; 31 import java.util.Objects; 32 33 /** Route of the exercise session. Contains sequence of location points with timestamps. */ 34 public final class ExerciseRoute implements Parcelable { 35 private final List<Location> mRouteLocations; 36 37 @NonNull 38 public static final Creator<ExerciseRoute> CREATOR = 39 new Creator<>() { 40 41 @Override 42 public ExerciseRoute createFromParcel(Parcel source) { 43 int size = source.readInt(); 44 List<Location> locations = new ArrayList<>(size); 45 for (int i = 0; i < size; i++) { 46 locations.add(i, Location.CREATOR.createFromParcel(source)); 47 } 48 return new ExerciseRoute(locations); 49 } 50 51 @Override 52 public ExerciseRoute[] newArray(int size) { 53 return new ExerciseRoute[size]; 54 } 55 }; 56 57 /** 58 * Creates {@link ExerciseRoute} instance 59 * 60 * @param routeLocations list of locations with timestamps that make up the route 61 */ ExerciseRoute(@onNull List<Location> routeLocations)62 public ExerciseRoute(@NonNull List<Location> routeLocations) { 63 Objects.requireNonNull(routeLocations); 64 mRouteLocations = routeLocations; 65 } 66 67 @NonNull getRouteLocations()68 public List<Location> getRouteLocations() { 69 return mRouteLocations; 70 } 71 72 @Override equals(Object o)73 public boolean equals(Object o) { 74 if (this == o) return true; 75 if (!(o instanceof ExerciseRoute)) return false; 76 ExerciseRoute that = (ExerciseRoute) o; 77 return getRouteLocations().equals(that.getRouteLocations()); 78 } 79 80 @Override hashCode()81 public int hashCode() { 82 return Objects.hash(getRouteLocations()); 83 } 84 85 /** @hide */ toRouteInternal()86 public ExerciseRouteInternal toRouteInternal() { 87 List<ExerciseRouteInternal.LocationInternal> routeLocations = 88 new ArrayList<>(getRouteLocations().size()); 89 for (ExerciseRoute.Location location : getRouteLocations()) { 90 routeLocations.add(location.toExerciseRouteLocationInternal()); 91 } 92 return new ExerciseRouteInternal(routeLocations); 93 } 94 95 @Override describeContents()96 public int describeContents() { 97 return 0; 98 } 99 100 @Override writeToParcel(@onNull Parcel dest, int flags)101 public void writeToParcel(@NonNull Parcel dest, int flags) { 102 dest.writeInt(mRouteLocations.size()); 103 for (int i = 0; i < mRouteLocations.size(); i++) { 104 mRouteLocations.get(i).writeToParcel(dest, flags); 105 } 106 } 107 108 /** Point in the time and space. Used in {@link ExerciseRoute}. */ 109 public static final class Location implements Parcelable { 110 // Values are used for FloatRange annotation in latitude/longitude getters and constructor. 111 private static final double MIN_LONGITUDE = -180; 112 private static final double MAX_LONGITUDE = 180; 113 private static final double MIN_LATITUDE = -90; 114 private static final double MAX_LATITUDE = 90; 115 116 private final Instant mTime; 117 private final double mLatitude; 118 private final double mLongitude; 119 private final Length mHorizontalAccuracy; 120 private final Length mVerticalAccuracy; 121 private final Length mAltitude; 122 123 @NonNull 124 public static final Creator<Location> CREATOR = 125 new Creator<>() { 126 @Override 127 public Location createFromParcel(Parcel source) { 128 Instant timestamp = Instant.ofEpochMilli(source.readLong()); 129 double lat = source.readDouble(); 130 double lon = source.readDouble(); 131 Builder builder = new Builder(timestamp, lat, lon); 132 if (source.readBoolean()) { 133 builder.setHorizontalAccuracy(Length.fromMeters(source.readDouble())); 134 } 135 if (source.readBoolean()) { 136 builder.setVerticalAccuracy(Length.fromMeters(source.readDouble())); 137 } 138 if (source.readBoolean()) { 139 builder.setAltitude(Length.fromMeters(source.readDouble())); 140 } 141 return builder.build(); 142 } 143 144 @Override 145 public Location[] newArray(int size) { 146 return new Location[size]; 147 } 148 }; 149 150 /** 151 * Represents a single location in an exercise route. 152 * 153 * @param time The point in time when the measurement was taken. 154 * @param latitude Latitude of a location represented as a double, in degrees. Valid range: 155 * from -90 to 90 degrees. 156 * @param longitude Longitude of a location represented as a double, in degrees. Valid 157 * range: from -180 to 180 degrees. 158 * @param horizontalAccuracy The radius of uncertainty for the location, in [Length] unit. 159 * Must be non-negative value. 160 * @param verticalAccuracy The validity of the altitude values, and their estimated 161 * uncertainty, in [Length] unit. Must be non-negative value. 162 * @param altitude An altitude of a location represented as a float, in [Length] unit above 163 * sea level. 164 * @param skipValidation Boolean flag to skip validation of record values. 165 * @see ExerciseRoute 166 */ Location( @onNull 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)167 private Location( 168 @NonNull Instant time, 169 @FloatRange(from = MIN_LATITUDE, to = MAX_LATITUDE) double latitude, 170 @FloatRange(from = MIN_LONGITUDE, to = MAX_LONGITUDE) double longitude, 171 @Nullable Length horizontalAccuracy, 172 @Nullable Length verticalAccuracy, 173 @Nullable Length altitude, 174 boolean skipValidation) { 175 Objects.requireNonNull(time); 176 177 if (!skipValidation) { 178 ValidationUtils.requireInRange(latitude, MIN_LATITUDE, MAX_LATITUDE, "Latitude"); 179 ValidationUtils.requireInRange( 180 longitude, MIN_LONGITUDE, MAX_LONGITUDE, "Longitude"); 181 182 if (horizontalAccuracy != null) { 183 ValidationUtils.requireNonNegative( 184 horizontalAccuracy.getInMeters(), "Horizontal accuracy"); 185 } 186 187 if (verticalAccuracy != null && verticalAccuracy.getInMeters() < 0) { 188 ValidationUtils.requireNonNegative( 189 verticalAccuracy.getInMeters(), "Vertical accuracy"); 190 } 191 } 192 193 mTime = time; 194 mLatitude = latitude; 195 mLongitude = longitude; 196 mHorizontalAccuracy = horizontalAccuracy; 197 mVerticalAccuracy = verticalAccuracy; 198 mAltitude = altitude; 199 } 200 201 /** Returns time when this location has been recorded */ 202 @NonNull getTime()203 public Instant getTime() { 204 return mTime; 205 } 206 207 /** Returns longitude of this location */ 208 @FloatRange(from = -180.0, to = 180.0) getLongitude()209 public double getLongitude() { 210 return mLongitude; 211 } 212 213 /** Returns latitude of this location */ 214 @FloatRange(from = -90.0, to = 90.0) getLatitude()215 public double getLatitude() { 216 return mLatitude; 217 } 218 219 /** 220 * Returns horizontal accuracy of this location time point. Returns null if no horizontal 221 * accuracy was specified. 222 */ 223 @Nullable getHorizontalAccuracy()224 public Length getHorizontalAccuracy() { 225 return mHorizontalAccuracy; 226 } 227 228 /** 229 * Returns vertical accuracy of this location time point. Returns null if no vertical 230 * accuracy was specified. 231 */ 232 @Nullable getVerticalAccuracy()233 public Length getVerticalAccuracy() { 234 return mVerticalAccuracy; 235 } 236 237 /** 238 * Returns altitude of this location time point. Returns null if no altitude was specified. 239 */ 240 @Nullable getAltitude()241 public Length getAltitude() { 242 return mAltitude; 243 } 244 245 /** @hide */ toExerciseRouteLocationInternal()246 public ExerciseRouteInternal.LocationInternal toExerciseRouteLocationInternal() { 247 ExerciseRouteInternal.LocationInternal locationInternal = 248 new ExerciseRouteInternal.LocationInternal() 249 .setTime(getTime().toEpochMilli()) 250 .setLatitude(getLatitude()) 251 .setLongitude(getLongitude()); 252 253 if (getHorizontalAccuracy() != null) { 254 locationInternal.setHorizontalAccuracy(getHorizontalAccuracy().getInMeters()); 255 } 256 257 if (getVerticalAccuracy() != null) { 258 locationInternal.setVerticalAccuracy(getVerticalAccuracy().getInMeters()); 259 } 260 261 if (getAltitude() != null) { 262 locationInternal.setAltitude(getAltitude().getInMeters()); 263 } 264 return locationInternal; 265 } 266 267 @Override equals(Object o)268 public boolean equals(Object o) { 269 if (this == o) return true; 270 if (!(o instanceof Location)) return false; 271 Location that = (Location) o; 272 return Objects.equals(getAltitude(), that.getAltitude()) 273 && getTime().equals(that.getTime()) 274 && (getLatitude() == that.getLatitude()) 275 && (getLongitude() == that.getLongitude()) 276 && Objects.equals(getHorizontalAccuracy(), that.getHorizontalAccuracy()) 277 && Objects.equals(getVerticalAccuracy(), that.getVerticalAccuracy()); 278 } 279 280 @Override hashCode()281 public int hashCode() { 282 return Objects.hash( 283 getTime(), 284 getLatitude(), 285 getLongitude(), 286 getHorizontalAccuracy(), 287 getVerticalAccuracy(), 288 getAltitude()); 289 } 290 291 @Override describeContents()292 public int describeContents() { 293 return 0; 294 } 295 296 @Override writeToParcel(@onNull Parcel dest, int flags)297 public void writeToParcel(@NonNull Parcel dest, int flags) { 298 dest.writeLong(mTime.toEpochMilli()); 299 dest.writeDouble(mLatitude); 300 dest.writeDouble(mLongitude); 301 dest.writeBoolean(mHorizontalAccuracy != null); 302 if (mHorizontalAccuracy != null) { 303 dest.writeDouble(mHorizontalAccuracy.getInMeters()); 304 } 305 dest.writeBoolean(mVerticalAccuracy != null); 306 if (mVerticalAccuracy != null) { 307 dest.writeDouble(mVerticalAccuracy.getInMeters()); 308 } 309 dest.writeBoolean(mAltitude != null); 310 if (mAltitude != null) { 311 dest.writeDouble(mAltitude.getInMeters()); 312 } 313 } 314 315 /** Builder class for {@link Location} */ 316 public static final class Builder { 317 @NonNull private final Instant mTime; 318 319 @FloatRange(from = MIN_LATITUDE, to = MAX_LATITUDE) 320 private final double mLatitude; 321 322 @FloatRange(from = MIN_LONGITUDE, to = MAX_LONGITUDE) 323 private final double mLongitude; 324 325 @Nullable private Length mHorizontalAccuracy; 326 @Nullable private Length mVerticalAccuracy; 327 @Nullable private Length mAltitude; 328 329 /** Sets time, longitude and latitude to the point. */ Builder( @onNull Instant time, @FloatRange(from = -90.0, to = 90.0) double latitude, @FloatRange(from = -180.0, to = 180.0) double longitude)330 public Builder( 331 @NonNull Instant time, 332 @FloatRange(from = -90.0, to = 90.0) double latitude, 333 @FloatRange(from = -180.0, to = 180.0) double longitude) { 334 Objects.requireNonNull(time); 335 mTime = time; 336 mLatitude = latitude; 337 mLongitude = longitude; 338 } 339 340 /** Sets horizontal accuracy to the point. */ 341 @NonNull setHorizontalAccuracy(@onNull Length horizontalAccuracy)342 public Builder setHorizontalAccuracy(@NonNull Length horizontalAccuracy) { 343 Objects.requireNonNull(horizontalAccuracy); 344 mHorizontalAccuracy = horizontalAccuracy; 345 return this; 346 } 347 348 /** Sets vertical accuracy to the point. */ 349 @NonNull setVerticalAccuracy(@onNull Length verticalAccuracy)350 public Builder setVerticalAccuracy(@NonNull Length verticalAccuracy) { 351 Objects.requireNonNull(verticalAccuracy); 352 mVerticalAccuracy = verticalAccuracy; 353 return this; 354 } 355 356 /** Sets altitude to the point. */ 357 @NonNull setAltitude(@onNull Length altitude)358 public Builder setAltitude(@NonNull Length altitude) { 359 Objects.requireNonNull(altitude); 360 mAltitude = altitude; 361 return this; 362 } 363 364 /** 365 * @return Object of {@link Location} without validating the values. 366 * @hide 367 */ 368 @NonNull buildWithoutValidation()369 public Location buildWithoutValidation() { 370 return new Location( 371 mTime, 372 mLatitude, 373 mLongitude, 374 mHorizontalAccuracy, 375 mVerticalAccuracy, 376 mAltitude, 377 true); 378 } 379 380 /** Builds {@link Location} */ 381 @NonNull build()382 public Location build() { 383 return new Location( 384 mTime, 385 mLatitude, 386 mLongitude, 387 mHorizontalAccuracy, 388 mVerticalAccuracy, 389 mAltitude, 390 false); 391 } 392 } 393 } 394 } 395