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