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