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 */ 167 @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression 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)168 private Location( 169 @NonNull Instant time, 170 @FloatRange(from = MIN_LATITUDE, to = MAX_LATITUDE) double latitude, 171 @FloatRange(from = MIN_LONGITUDE, to = MAX_LONGITUDE) double longitude, 172 @Nullable Length horizontalAccuracy, 173 @Nullable Length verticalAccuracy, 174 @Nullable Length altitude, 175 boolean skipValidation) { 176 Objects.requireNonNull(time); 177 178 if (!skipValidation) { 179 ValidationUtils.requireInRange(latitude, MIN_LATITUDE, MAX_LATITUDE, "Latitude"); 180 ValidationUtils.requireInRange( 181 longitude, MIN_LONGITUDE, MAX_LONGITUDE, "Longitude"); 182 183 if (horizontalAccuracy != null) { 184 ValidationUtils.requireNonNegative( 185 horizontalAccuracy.getInMeters(), "Horizontal accuracy"); 186 } 187 188 if (verticalAccuracy != null && verticalAccuracy.getInMeters() < 0) { 189 ValidationUtils.requireNonNegative( 190 verticalAccuracy.getInMeters(), "Vertical accuracy"); 191 } 192 } 193 194 mTime = time; 195 mLatitude = latitude; 196 mLongitude = longitude; 197 mHorizontalAccuracy = horizontalAccuracy; 198 mVerticalAccuracy = verticalAccuracy; 199 mAltitude = altitude; 200 } 201 202 /** Returns time when this location has been recorded */ 203 @NonNull getTime()204 public Instant getTime() { 205 return mTime; 206 } 207 208 /** Returns longitude of this location */ 209 @FloatRange(from = -180.0, to = 180.0) getLongitude()210 public double getLongitude() { 211 return mLongitude; 212 } 213 214 /** Returns latitude of this location */ 215 @FloatRange(from = -90.0, to = 90.0) getLatitude()216 public double getLatitude() { 217 return mLatitude; 218 } 219 220 /** 221 * Returns horizontal accuracy of this location time point. Returns null if no horizontal 222 * accuracy was specified. 223 */ 224 @Nullable getHorizontalAccuracy()225 public Length getHorizontalAccuracy() { 226 return mHorizontalAccuracy; 227 } 228 229 /** 230 * Returns vertical accuracy of this location time point. Returns null if no vertical 231 * accuracy was specified. 232 */ 233 @Nullable getVerticalAccuracy()234 public Length getVerticalAccuracy() { 235 return mVerticalAccuracy; 236 } 237 238 /** 239 * Returns altitude of this location time point. Returns null if no altitude was specified. 240 */ 241 @Nullable getAltitude()242 public Length getAltitude() { 243 return mAltitude; 244 } 245 246 /** @hide */ toExerciseRouteLocationInternal()247 public ExerciseRouteInternal.LocationInternal toExerciseRouteLocationInternal() { 248 ExerciseRouteInternal.LocationInternal locationInternal = 249 new ExerciseRouteInternal.LocationInternal() 250 .setTime(getTime().toEpochMilli()) 251 .setLatitude(getLatitude()) 252 .setLongitude(getLongitude()); 253 254 if (getHorizontalAccuracy() != null) { 255 locationInternal.setHorizontalAccuracy(getHorizontalAccuracy().getInMeters()); 256 } 257 258 if (getVerticalAccuracy() != null) { 259 locationInternal.setVerticalAccuracy(getVerticalAccuracy().getInMeters()); 260 } 261 262 if (getAltitude() != null) { 263 locationInternal.setAltitude(getAltitude().getInMeters()); 264 } 265 return locationInternal; 266 } 267 268 @Override equals(Object o)269 public boolean equals(Object o) { 270 if (this == o) return true; 271 if (!(o instanceof Location)) return false; 272 Location that = (Location) o; 273 return Objects.equals(getAltitude(), that.getAltitude()) 274 && getTime().equals(that.getTime()) 275 && (getLatitude() == that.getLatitude()) 276 && (getLongitude() == that.getLongitude()) 277 && Objects.equals(getHorizontalAccuracy(), that.getHorizontalAccuracy()) 278 && Objects.equals(getVerticalAccuracy(), that.getVerticalAccuracy()); 279 } 280 281 @Override hashCode()282 public int hashCode() { 283 return Objects.hash( 284 getTime(), 285 getLatitude(), 286 getLongitude(), 287 getHorizontalAccuracy(), 288 getVerticalAccuracy(), 289 getAltitude()); 290 } 291 292 @Override describeContents()293 public int describeContents() { 294 return 0; 295 } 296 297 @Override writeToParcel(@onNull Parcel dest, int flags)298 public void writeToParcel(@NonNull Parcel dest, int flags) { 299 dest.writeLong(mTime.toEpochMilli()); 300 dest.writeDouble(mLatitude); 301 dest.writeDouble(mLongitude); 302 dest.writeBoolean(mHorizontalAccuracy != null); 303 if (mHorizontalAccuracy != null) { 304 dest.writeDouble(mHorizontalAccuracy.getInMeters()); 305 } 306 dest.writeBoolean(mVerticalAccuracy != null); 307 if (mVerticalAccuracy != null) { 308 dest.writeDouble(mVerticalAccuracy.getInMeters()); 309 } 310 dest.writeBoolean(mAltitude != null); 311 if (mAltitude != null) { 312 dest.writeDouble(mAltitude.getInMeters()); 313 } 314 } 315 316 /** Builder class for {@link Location} */ 317 public static final class Builder { 318 @NonNull private final Instant mTime; 319 320 @FloatRange(from = MIN_LATITUDE, to = MAX_LATITUDE) 321 private final double mLatitude; 322 323 @FloatRange(from = MIN_LONGITUDE, to = MAX_LONGITUDE) 324 private final double mLongitude; 325 326 @Nullable private Length mHorizontalAccuracy; 327 @Nullable private Length mVerticalAccuracy; 328 @Nullable private Length mAltitude; 329 330 /** 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)331 public Builder( 332 @NonNull Instant time, 333 @FloatRange(from = -90.0, to = 90.0) double latitude, 334 @FloatRange(from = -180.0, to = 180.0) double longitude) { 335 Objects.requireNonNull(time); 336 mTime = time; 337 mLatitude = latitude; 338 mLongitude = longitude; 339 } 340 341 /** Sets horizontal accuracy to the point. */ 342 @NonNull setHorizontalAccuracy(@onNull Length horizontalAccuracy)343 public Builder setHorizontalAccuracy(@NonNull Length horizontalAccuracy) { 344 Objects.requireNonNull(horizontalAccuracy); 345 mHorizontalAccuracy = horizontalAccuracy; 346 return this; 347 } 348 349 /** Sets vertical accuracy to the point. */ 350 @NonNull setVerticalAccuracy(@onNull Length verticalAccuracy)351 public Builder setVerticalAccuracy(@NonNull Length verticalAccuracy) { 352 Objects.requireNonNull(verticalAccuracy); 353 mVerticalAccuracy = verticalAccuracy; 354 return this; 355 } 356 357 /** Sets altitude to the point. */ 358 @NonNull setAltitude(@onNull Length altitude)359 public Builder setAltitude(@NonNull Length altitude) { 360 Objects.requireNonNull(altitude); 361 mAltitude = altitude; 362 return this; 363 } 364 365 /** 366 * @return Object of {@link Location} without validating the values. 367 * @hide 368 */ 369 @NonNull buildWithoutValidation()370 public Location buildWithoutValidation() { 371 return new Location( 372 mTime, 373 mLatitude, 374 mLongitude, 375 mHorizontalAccuracy, 376 mVerticalAccuracy, 377 mAltitude, 378 true); 379 } 380 381 /** Builds {@link Location} */ 382 @NonNull build()383 public Location build() { 384 return new Location( 385 mTime, 386 mLatitude, 387 mLongitude, 388 mHorizontalAccuracy, 389 mVerticalAccuracy, 390 mAltitude, 391 false); 392 } 393 } 394 } 395 } 396