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