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