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 package android.health.connect.datatypes;
17 
18 import android.annotation.NonNull;
19 import android.annotation.Nullable;
20 import android.health.connect.HealthConnectManager;
21 import android.health.connect.datatypes.units.Velocity;
22 import android.health.connect.datatypes.validation.ValidationUtils;
23 import android.health.connect.internal.datatypes.SpeedRecordInternal;
24 
25 import java.time.Instant;
26 import java.time.ZoneOffset;
27 import java.util.HashSet;
28 import java.util.List;
29 import java.util.Objects;
30 import java.util.Set;
31 
32 /** Captures the user's speed, e.g. during running or cycling. */
33 @Identifier(recordIdentifier = RecordTypeIdentifier.RECORD_TYPE_SPEED)
34 public final class SpeedRecord extends IntervalRecord {
35 
36     /**
37      * Metric identifier to retrieve average speed using aggregate APIs in {@link
38      * HealthConnectManager}
39      */
40     @NonNull
41     public static final AggregationType<Velocity> SPEED_AVG =
42             new AggregationType<>(
43                     AggregationType.AggregationTypeIdentifier.SPEED_RECORD_SPEED_AVG,
44                     AggregationType.AVG,
45                     RecordTypeIdentifier.RECORD_TYPE_SPEED,
46                     Velocity.class);
47 
48     /**
49      * Metric identifier to retrieve minimum speed using aggregate APIs in {@link
50      * HealthConnectManager}
51      */
52     @NonNull
53     public static final AggregationType<Velocity> SPEED_MIN =
54             new AggregationType<>(
55                     AggregationType.AggregationTypeIdentifier.SPEED_RECORD_SPEED_MIN,
56                     AggregationType.MIN,
57                     RecordTypeIdentifier.RECORD_TYPE_SPEED,
58                     Velocity.class);
59 
60     /**
61      * Metric identifier to retrieve maximum speed using aggregate APIs in {@link
62      * HealthConnectManager}
63      */
64     @NonNull
65     public static final AggregationType<Velocity> SPEED_MAX =
66             new AggregationType<>(
67                     AggregationType.AggregationTypeIdentifier.SPEED_RECORD_SPEED_MAX,
68                     AggregationType.MAX,
69                     RecordTypeIdentifier.RECORD_TYPE_SPEED,
70                     Velocity.class);
71 
72     private final List<SpeedRecordSample> mSpeedRecordSamples;
73 
74     /**
75      * @param metadata Metadata to be associated with the record. See {@link Metadata}.
76      * @param startTime Start time of this activity
77      * @param startZoneOffset Zone offset of the user when the activity started
78      * @param endTime End time of this activity
79      * @param endZoneOffset Zone offset of the user when the activity finished
80      * @param speedRecordSamples Samples of recorded SpeedRecord
81      * @param skipValidation Boolean flag to skip validation of record values.
82      */
SpeedRecord( @onNull Metadata metadata, @NonNull Instant startTime, @NonNull ZoneOffset startZoneOffset, @NonNull Instant endTime, @NonNull ZoneOffset endZoneOffset, @NonNull List<SpeedRecordSample> speedRecordSamples, boolean skipValidation)83     private SpeedRecord(
84             @NonNull Metadata metadata,
85             @NonNull Instant startTime,
86             @NonNull ZoneOffset startZoneOffset,
87             @NonNull Instant endTime,
88             @NonNull ZoneOffset endZoneOffset,
89             @NonNull List<SpeedRecordSample> speedRecordSamples,
90             boolean skipValidation) {
91         super(
92                 metadata,
93                 startTime,
94                 startZoneOffset,
95                 endTime,
96                 endZoneOffset,
97                 skipValidation,
98                 /* enforceFutureTimeRestrictions= */ true);
99         Objects.requireNonNull(speedRecordSamples);
100         if (!skipValidation) {
101             ValidationUtils.validateSampleStartAndEndTime(
102                     startTime,
103                     endTime,
104                     speedRecordSamples.stream()
105                             .map(SpeedRecord.SpeedRecordSample::getTime)
106                             .toList());
107         }
108         mSpeedRecordSamples = speedRecordSamples;
109     }
110 
111     /**
112      * @return SpeedRecord samples corresponding to this record
113      */
114     @NonNull
getSamples()115     public List<SpeedRecordSample> getSamples() {
116         return mSpeedRecordSamples;
117     }
118 
119     /** Represents a single measurement of the speed, a scalar magnitude. */
120     public static final class SpeedRecordSample {
121         private final Velocity mSpeed;
122         private final Instant mTime;
123 
124         /**
125          * SpeedRecord sample for entries of {@link SpeedRecord}
126          *
127          * @param speed Speed in {@link Velocity} unit.
128          * @param time The point in time when the measurement was taken.
129          */
SpeedRecordSample(@onNull Velocity speed, @NonNull Instant time)130         public SpeedRecordSample(@NonNull Velocity speed, @NonNull Instant time) {
131             this(speed, time, false);
132         }
133 
134         /**
135          * SpeedRecord sample for entries of {@link SpeedRecord}
136          *
137          * @param speed Speed in {@link Velocity} unit.
138          * @param time The point in time when the measurement was taken.
139          * @param skipValidation Boolean flag to skip validation of record values.
140          * @hide
141          */
SpeedRecordSample( @onNull Velocity speed, @NonNull Instant time, boolean skipValidation)142         public SpeedRecordSample(
143                 @NonNull Velocity speed, @NonNull Instant time, boolean skipValidation) {
144             Objects.requireNonNull(time);
145             Objects.requireNonNull(speed);
146             if (!skipValidation) {
147                 ValidationUtils.requireInRange(speed.getInMetersPerSecond(), 0.0, 1000000, "speed");
148             }
149             mTime = time;
150             mSpeed = speed;
151         }
152 
153         /**
154          * @return Speed for this sample
155          */
156         @NonNull
getSpeed()157         public Velocity getSpeed() {
158             return mSpeed;
159         }
160 
161         /**
162          * @return time at which this sample was recorded
163          */
164         @NonNull
getTime()165         public Instant getTime() {
166             return mTime;
167         }
168 
169         /**
170          * Indicates whether some other object is "equal to" this one.
171          *
172          * @param object the reference object with which to compare.
173          * @return {@code true} if this object is the same as the obj
174          */
175         @Override
equals(@ullable Object object)176         public boolean equals(@Nullable Object object) {
177             if (super.equals(object) && object instanceof SpeedRecordSample) {
178                 SpeedRecordSample other = (SpeedRecordSample) object;
179                 return getSpeed().equals(other.getSpeed())
180                         && getTime().toEpochMilli() == other.getTime().toEpochMilli();
181             }
182             return false;
183         }
184 
185         /**
186          * Returns a hash code value for the object.
187          *
188          * @return a hash code value for this object.
189          */
190         @Override
hashCode()191         public int hashCode() {
192             return Objects.hash(super.hashCode(), getSpeed(), getTime());
193         }
194     }
195 
196     /** Builder class for {@link SpeedRecord} */
197     public static final class Builder {
198         private final Metadata mMetadata;
199         private final Instant mStartTime;
200         private final Instant mEndTime;
201         private final List<SpeedRecordSample> mSpeedRecordSamples;
202         private ZoneOffset mStartZoneOffset;
203         private ZoneOffset mEndZoneOffset;
204 
205         /**
206          * @param metadata Metadata to be associated with the record. See {@link Metadata}.
207          * @param startTime Start time of this activity
208          * @param endTime End time of this activity
209          * @param speedRecordSamples Samples of recorded SpeedRecord
210          */
Builder( @onNull Metadata metadata, @NonNull Instant startTime, @NonNull Instant endTime, @NonNull List<SpeedRecordSample> speedRecordSamples)211         public Builder(
212                 @NonNull Metadata metadata,
213                 @NonNull Instant startTime,
214                 @NonNull Instant endTime,
215                 @NonNull List<SpeedRecordSample> speedRecordSamples) {
216             Objects.requireNonNull(metadata);
217             Objects.requireNonNull(startTime);
218             Objects.requireNonNull(endTime);
219             Objects.requireNonNull(speedRecordSamples);
220             mMetadata = metadata;
221             mStartTime = startTime;
222             mEndTime = endTime;
223             mSpeedRecordSamples = speedRecordSamples;
224             mStartZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(startTime);
225             mEndZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(endTime);
226         }
227 
228         /** Sets the zone offset of the user when the activity started */
229         @NonNull
setStartZoneOffset(@onNull ZoneOffset startZoneOffset)230         public Builder setStartZoneOffset(@NonNull ZoneOffset startZoneOffset) {
231             Objects.requireNonNull(startZoneOffset);
232             mStartZoneOffset = startZoneOffset;
233             return this;
234         }
235 
236         /** Sets the zone offset of the user when the activity ended */
237         @NonNull
setEndZoneOffset(@onNull ZoneOffset endZoneOffset)238         public Builder setEndZoneOffset(@NonNull ZoneOffset endZoneOffset) {
239             Objects.requireNonNull(endZoneOffset);
240             mEndZoneOffset = endZoneOffset;
241             return this;
242         }
243 
244         /** Sets the start zone offset of this record to system default. */
245         @NonNull
clearStartZoneOffset()246         public Builder clearStartZoneOffset() {
247             mStartZoneOffset = RecordUtils.getDefaultZoneOffset();
248             return this;
249         }
250 
251         /** Sets the start zone offset of this record to system default. */
252         @NonNull
clearEndZoneOffset()253         public Builder clearEndZoneOffset() {
254             mEndZoneOffset = RecordUtils.getDefaultZoneOffset();
255             return this;
256         }
257 
258         /**
259          * @return Object of {@link SpeedRecord} without validating the values.
260          * @hide
261          */
262         @NonNull
buildWithoutValidation()263         public SpeedRecord buildWithoutValidation() {
264             return new SpeedRecord(
265                     mMetadata,
266                     mStartTime,
267                     mStartZoneOffset,
268                     mEndTime,
269                     mEndZoneOffset,
270                     mSpeedRecordSamples,
271                     true);
272         }
273 
274         /**
275          * @return Object of {@link SpeedRecord}
276          */
277         @NonNull
build()278         public SpeedRecord build() {
279             return new SpeedRecord(
280                     mMetadata,
281                     mStartTime,
282                     mStartZoneOffset,
283                     mEndTime,
284                     mEndZoneOffset,
285                     mSpeedRecordSamples,
286                     false);
287         }
288     }
289 
290     /**
291      * Indicates whether some other object is "equal to" this one.
292      *
293      * @param object the reference object with which to compare.
294      * @return {@code true} if this object is the same as the obj
295      */
296     @Override
equals(@ullable Object object)297     public boolean equals(@Nullable Object object) {
298         if (super.equals(object) && object instanceof SpeedRecord) {
299             SpeedRecord other = (SpeedRecord) object;
300             if (getSamples().size() != other.getSamples().size()) return false;
301             for (int idx = 0; idx < getSamples().size(); idx++) {
302                 if (!Objects.equals(
303                                 getSamples().get(idx).getSpeed(),
304                                 other.getSamples().get(idx).getSpeed())
305                         || getSamples().get(idx).getTime().toEpochMilli()
306                                 != other.getSamples().get(idx).getTime().toEpochMilli()) {
307                     return false;
308                 }
309             }
310             return true;
311         }
312         return false;
313     }
314 
315     /** Returns a hash code value for the object. */
316     @Override
hashCode()317     public int hashCode() {
318         return Objects.hash(super.hashCode(), getSamples());
319     }
320 
321     /** @hide */
322     @Override
toRecordInternal()323     public SpeedRecordInternal toRecordInternal() {
324         SpeedRecordInternal recordInternal =
325                 (SpeedRecordInternal)
326                         new SpeedRecordInternal()
327                                 .setUuid(getMetadata().getId())
328                                 .setPackageName(getMetadata().getDataOrigin().getPackageName())
329                                 .setLastModifiedTime(
330                                         getMetadata().getLastModifiedTime().toEpochMilli())
331                                 .setClientRecordId(getMetadata().getClientRecordId())
332                                 .setClientRecordVersion(getMetadata().getClientRecordVersion())
333                                 .setManufacturer(getMetadata().getDevice().getManufacturer())
334                                 .setModel(getMetadata().getDevice().getModel())
335                                 .setDeviceType(getMetadata().getDevice().getType())
336                                 .setRecordingMethod(getMetadata().getRecordingMethod());
337         Set<SpeedRecordInternal.SpeedRecordSample> samples = new HashSet<>(getSamples().size());
338 
339         for (SpeedRecord.SpeedRecordSample speedRecordSample : getSamples()) {
340             samples.add(
341                     new SpeedRecordInternal.SpeedRecordSample(
342                             speedRecordSample.getSpeed().getInMetersPerSecond(),
343                             speedRecordSample.getTime().toEpochMilli()));
344         }
345         recordInternal.setSamples(samples);
346         recordInternal.setStartTime(getStartTime().toEpochMilli());
347         recordInternal.setEndTime(getEndTime().toEpochMilli());
348         recordInternal.setStartZoneOffset(getStartZoneOffset().getTotalSeconds());
349         recordInternal.setEndZoneOffset(getEndZoneOffset().getTotalSeconds());
350 
351         return recordInternal;
352     }
353 }
354