1 /*
2  * Copyright (C) 2024 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 static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_SKIN_TEMPERATURE;
20 import static android.health.connect.datatypes.validation.ValidationUtils.validateIntDefValue;
21 
22 import static java.util.Objects.requireNonNull;
23 import static java.util.stream.Collectors.toList;
24 
25 import android.annotation.FlaggedApi;
26 import android.annotation.IntDef;
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.health.connect.HealthConnectManager;
30 import android.health.connect.datatypes.units.Temperature;
31 import android.health.connect.datatypes.units.TemperatureDelta;
32 import android.health.connect.datatypes.validation.ValidationUtils;
33 import android.health.connect.internal.datatypes.SkinTemperatureRecordInternal;
34 
35 import java.lang.annotation.Retention;
36 import java.lang.annotation.RetentionPolicy;
37 import java.time.Instant;
38 import java.time.ZoneOffset;
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.Comparator;
42 import java.util.List;
43 import java.util.Objects;
44 import java.util.Set;
45 import java.util.stream.Collectors;
46 
47 /**
48  * Captures the skin temperature of a user. Each record can represent a series of measurements of
49  * temperature differences.
50  */
51 @Identifier(recordIdentifier = RECORD_TYPE_SKIN_TEMPERATURE)
52 @FlaggedApi("com.android.healthconnect.flags.skin_temperature")
53 public final class SkinTemperatureRecord extends IntervalRecord {
54     /** Skin temperature measurement location unknown. */
55     public static final int MEASUREMENT_LOCATION_UNKNOWN = 0;
56     /** Skin temperature measurement location finger. */
57     public static final int MEASUREMENT_LOCATION_FINGER = 1;
58     /** Skin temperature measurement location toe. */
59     public static final int MEASUREMENT_LOCATION_TOE = 2;
60     /** Skin temperature measurement location wrist. */
61     public static final int MEASUREMENT_LOCATION_WRIST = 3;
62     /**
63      * Metric identifier to retrieve average skin temperature delta using aggregate APIs in {@link
64      * HealthConnectManager}.
65      */
66     @NonNull
67     public static final AggregationType<TemperatureDelta> SKIN_TEMPERATURE_DELTA_AVG =
68             new AggregationType<>(
69                     AggregationType.AggregationTypeIdentifier.SKIN_TEMPERATURE_RECORD_DELTA_AVG,
70                     AggregationType.AVG,
71                     RECORD_TYPE_SKIN_TEMPERATURE,
72                     TemperatureDelta.class);
73     /**
74      * Metric identifier to retrieve minimum skin temperature delta using aggregate APIs in {@link
75      * HealthConnectManager}.
76      */
77     @NonNull
78     public static final AggregationType<TemperatureDelta> SKIN_TEMPERATURE_DELTA_MIN =
79             new AggregationType<>(
80                     AggregationType.AggregationTypeIdentifier.SKIN_TEMPERATURE_RECORD_DELTA_MIN,
81                     AggregationType.MIN,
82                     RECORD_TYPE_SKIN_TEMPERATURE,
83                     TemperatureDelta.class);
84     /**
85      * Metric identifier to retrieve maximum skin temperature delta using aggregate APIs in {@link
86      * HealthConnectManager}.
87      */
88     @NonNull
89     public static final AggregationType<TemperatureDelta> SKIN_TEMPERATURE_DELTA_MAX =
90             new AggregationType<>(
91                     AggregationType.AggregationTypeIdentifier.SKIN_TEMPERATURE_RECORD_DELTA_MAX,
92                     AggregationType.MAX,
93                     RECORD_TYPE_SKIN_TEMPERATURE,
94                     TemperatureDelta.class);
95 
96     @Nullable private final Temperature mBaseline;
97     @NonNull private final List<Delta> mDeltas;
98     private @SkinTemperatureMeasurementLocation int mMeasurementLocation;
99 
100     /**
101      * @param metadata Metadata to be associated with the record. See {@link Metadata}.
102      * @param startTime Start time of this activity.
103      * @param startZoneOffset Zone offset of the user when the activity started.
104      * @param endTime End time of this activity.
105      * @param endZoneOffset Zone offset of the user when the activity finished.
106      * @param baseline Baseline temperature of a skin temperature.
107      * @param deltas List of skin temperature deltas.
108      * @param measurementLocation Measurement location of the skin temperature.
109      * @param skipValidation Boolean flag to skip validation of record values.
110      */
SkinTemperatureRecord( @onNull Metadata metadata, @NonNull Instant startTime, @NonNull ZoneOffset startZoneOffset, @NonNull Instant endTime, @NonNull ZoneOffset endZoneOffset, @Nullable Temperature baseline, @NonNull List<Delta> deltas, @SkinTemperatureMeasurementLocation int measurementLocation, boolean skipValidation)111     private SkinTemperatureRecord(
112             @NonNull Metadata metadata,
113             @NonNull Instant startTime,
114             @NonNull ZoneOffset startZoneOffset,
115             @NonNull Instant endTime,
116             @NonNull ZoneOffset endZoneOffset,
117             @Nullable Temperature baseline,
118             @NonNull List<Delta> deltas,
119             @SkinTemperatureMeasurementLocation int measurementLocation,
120             boolean skipValidation) {
121         super(
122                 metadata,
123                 startTime,
124                 startZoneOffset,
125                 endTime,
126                 endZoneOffset,
127                 skipValidation,
128                 /* enforceFutureTimeRestrictions= */ true);
129         if (!skipValidation) {
130             if (baseline != null) {
131                 ValidationUtils.requireInRange(baseline.getInCelsius(), 0.0, 100, "temperature");
132             }
133             validateIntDefValue(
134                     measurementLocation,
135                     VALID_MEASUREMENT_LOCATIONS,
136                     SkinTemperatureMeasurementLocation.class.getSimpleName());
137             ValidationUtils.validateSampleStartAndEndTime(
138                     startTime, endTime, deltas.stream().map(Delta::getTime).toList());
139         }
140         mBaseline = baseline;
141         mDeltas =
142                 Collections.unmodifiableList(
143                         deltas.stream()
144                                 .sorted(Comparator.comparing(Delta::getTime))
145                                 .collect(toList()));
146         mMeasurementLocation = measurementLocation;
147     }
148 
149     /**
150      * @return baseline skin temperature in {@link Temperature} unit.
151      */
152     @Nullable
getBaseline()153     public Temperature getBaseline() {
154         return mBaseline;
155     }
156 
157     /**
158      * @return an unmodified list of skin temperature deltas in {@link TemperatureDelta} unit.
159      */
160     @NonNull
getDeltas()161     public List<Delta> getDeltas() {
162         return mDeltas;
163     }
164 
165     /**
166      * @return measurementLocation
167      */
168     @SkinTemperatureMeasurementLocation
getMeasurementLocation()169     public int getMeasurementLocation() {
170         return mMeasurementLocation;
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 object.
178      */
179     @Override
equals(@ullable Object object)180     public boolean equals(@Nullable Object object) {
181         if (this == object) return true;
182         if (!(object instanceof SkinTemperatureRecord)) return false;
183         if (!super.equals(object)) return false;
184         SkinTemperatureRecord that = (SkinTemperatureRecord) object;
185         if (!Objects.equals(getBaseline(), that.getBaseline())
186                 || getMeasurementLocation() != that.getMeasurementLocation()) return false;
187         return Objects.equals(getDeltas(), that.getDeltas());
188     }
189 
190     /**
191      * @return a hash code value for the object.
192      */
193     @Override
hashCode()194     public int hashCode() {
195         return Objects.hash(super.hashCode(), getBaseline(), getDeltas());
196     }
197 
198     /** @hide */
199     @Override
toRecordInternal()200     public SkinTemperatureRecordInternal toRecordInternal() {
201         SkinTemperatureRecordInternal recordInternal =
202                 (SkinTemperatureRecordInternal)
203                         new SkinTemperatureRecordInternal()
204                                 .setUuid(getMetadata().getId())
205                                 .setPackageName(getMetadata().getDataOrigin().getPackageName())
206                                 .setLastModifiedTime(
207                                         getMetadata().getLastModifiedTime().toEpochMilli())
208                                 .setClientRecordId(getMetadata().getClientRecordId())
209                                 .setClientRecordVersion(getMetadata().getClientRecordVersion())
210                                 .setManufacturer(getMetadata().getDevice().getManufacturer())
211                                 .setModel(getMetadata().getDevice().getModel())
212                                 .setDeviceType(getMetadata().getDevice().getType())
213                                 .setRecordingMethod(getMetadata().getRecordingMethod());
214         if (getBaseline() != null) {
215             recordInternal.setBaseline(getBaseline());
216         }
217 
218         recordInternal.setSamples(
219                 getDeltas().stream()
220                         .map(
221                                 delta ->
222                                         new SkinTemperatureRecordInternal
223                                                 .SkinTemperatureDeltaSample(
224                                                 delta.getDelta().getInCelsius(),
225                                                 delta.getTime().toEpochMilli()))
226                         .collect(Collectors.toSet()));
227         recordInternal.setMeasurementLocation(getMeasurementLocation());
228         recordInternal.setStartTime(getStartTime().toEpochMilli());
229         recordInternal.setEndTime(getEndTime().toEpochMilli());
230         recordInternal.setStartZoneOffset(getStartZoneOffset().getTotalSeconds());
231         recordInternal.setEndZoneOffset(getEndZoneOffset().getTotalSeconds());
232 
233         return recordInternal;
234     }
235 
236     /** Skin temperature delta entry of {@link SkinTemperatureRecord}. */
237     public static final class Delta {
238         private final TemperatureDelta mDelta;
239         private final Instant mTime;
240 
241         /**
242          * Skin temperature delta entry of {@link SkinTemperatureRecord}.
243          *
244          * @param delta Temperature difference.
245          * @param time The point in time when the measurement was taken.
246          */
Delta(@onNull TemperatureDelta delta, @NonNull Instant time)247         public Delta(@NonNull TemperatureDelta delta, @NonNull Instant time) {
248             this(delta, time, false);
249         }
250 
251         /**
252          * Skin temperature delta entry of {@link SkinTemperatureRecord}.
253          *
254          * @param delta Temperature difference.
255          * @param time The point in time when the measurement was taken.
256          * @param skipValidation Boolean flag to skip validation of record values.
257          * @hide
258          */
Delta( @onNull TemperatureDelta delta, @NonNull Instant time, boolean skipValidation)259         public Delta(
260                 @NonNull TemperatureDelta delta, @NonNull Instant time, boolean skipValidation) {
261             requireNonNull(delta);
262             requireNonNull(time);
263             if (!skipValidation) {
264                 ValidationUtils.requireInRange(delta.getInCelsius(), -30, 30, "temperature delta");
265             }
266             mDelta = delta;
267             mTime = time;
268         }
269 
270         /**
271          * @return delta of the skin temperature.
272          */
273         @NonNull
getDelta()274         public TemperatureDelta getDelta() {
275             return mDelta;
276         }
277 
278         /**
279          * @return time at which this measurement was recorded.
280          */
281         @NonNull
getTime()282         public Instant getTime() {
283             return mTime;
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 object.
291          */
292         @Override
equals(@ullable Object object)293         public boolean equals(@Nullable Object object) {
294             if (this == object) return true;
295             if (!(object instanceof Delta)) return false;
296             Delta that = (Delta) object;
297             return Objects.equals(getDelta(), that.getDelta())
298                     && getTime().toEpochMilli() == that.getTime().toEpochMilli();
299         }
300 
301         /**
302          * @return a hash code value for the object.
303          */
304         @Override
hashCode()305         public int hashCode() {
306             return Objects.hash(super.hashCode(), getDelta(), getTime());
307         }
308     }
309 
310     /**
311      * Valid set of values for this IntDef. Update this set when add new type or deprecate existing
312      * type.
313      *
314      * @hide
315      */
316     public static final Set<Integer> VALID_MEASUREMENT_LOCATIONS =
317             Set.of(
318                     MEASUREMENT_LOCATION_UNKNOWN,
319                     MEASUREMENT_LOCATION_FINGER,
320                     MEASUREMENT_LOCATION_TOE,
321                     MEASUREMENT_LOCATION_WRIST);
322 
323     /** @hide */
324     @IntDef({
325         MEASUREMENT_LOCATION_UNKNOWN,
326         MEASUREMENT_LOCATION_FINGER,
327         MEASUREMENT_LOCATION_TOE,
328         MEASUREMENT_LOCATION_WRIST,
329     })
330     @Retention(RetentionPolicy.SOURCE)
331     public @interface SkinTemperatureMeasurementLocation {}
332 
333     /** Builder class for {@link SkinTemperatureRecord} */
334     public static final class Builder {
335         private final Metadata mMetadata;
336         private final Instant mStartTime;
337         private final Instant mEndTime;
338         private ZoneOffset mStartZoneOffset;
339         private ZoneOffset mEndZoneOffset;
340         @Nullable private Temperature mBaseline;
341         private final List<Delta> mDeltas;
342         private int mMeasurementLocation;
343 
344         /**
345          * @param metadata Metadata Metadata to be associated with the record. See {@link Metadata}.
346          * @param startTime Start time of this activity.
347          * @param endTime End time of this activity.
348          */
Builder( @onNull Metadata metadata, @NonNull Instant startTime, @NonNull Instant endTime)349         public Builder(
350                 @NonNull Metadata metadata, @NonNull Instant startTime, @NonNull Instant endTime) {
351             requireNonNull(metadata);
352             requireNonNull(startTime);
353             requireNonNull(endTime);
354             mMetadata = metadata;
355             mStartTime = startTime;
356             mEndTime = endTime;
357             mDeltas = new ArrayList<>();
358             mMeasurementLocation = 0;
359             mStartZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(startTime);
360             mEndZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(endTime);
361         }
362 
363         /** Sets the zone offset of the user when the activity started. */
364         @NonNull
setStartZoneOffset(@onNull ZoneOffset startZoneOffset)365         public Builder setStartZoneOffset(@NonNull ZoneOffset startZoneOffset) {
366             requireNonNull(startZoneOffset);
367             mStartZoneOffset = startZoneOffset;
368             return this;
369         }
370 
371         /** Sets the zone offset of the user when the activity ended. */
372         @NonNull
setEndZoneOffset(@onNull ZoneOffset endZoneOffset)373         public Builder setEndZoneOffset(@NonNull ZoneOffset endZoneOffset) {
374             requireNonNull(endZoneOffset);
375             mEndZoneOffset = endZoneOffset;
376             return this;
377         }
378 
379         /** Clears the zone offset of the user when the activity started. */
380         @NonNull
clearStartZoneOffset()381         public Builder clearStartZoneOffset() {
382             mStartZoneOffset = RecordUtils.getDefaultZoneOffset();
383             return this;
384         }
385 
386         /** Clears the zone offset of the user when the activity ended. */
387         @NonNull
clearEndZoneOffset()388         public Builder clearEndZoneOffset() {
389             mEndZoneOffset = RecordUtils.getDefaultZoneOffset();
390             return this;
391         }
392 
393         /** Sets the baseline skin temperature for the user. */
394         @NonNull
setBaseline(@ullable Temperature baseline)395         public Builder setBaseline(@Nullable Temperature baseline) {
396             mBaseline = baseline;
397             return this;
398         }
399 
400         /** Sets a list of skin temperature deltas. */
401         @NonNull
setDeltas(@onNull List<Delta> deltas)402         public Builder setDeltas(@NonNull List<Delta> deltas) {
403             requireNonNull(deltas);
404             mDeltas.clear();
405             mDeltas.addAll(deltas);
406             return this;
407         }
408 
409         /** Sets the measurement location of the skin temperature. */
410         @NonNull
setMeasurementLocation( @kinTemperatureMeasurementLocation int measurementLocation)411         public Builder setMeasurementLocation(
412                 @SkinTemperatureMeasurementLocation int measurementLocation) {
413             mMeasurementLocation = measurementLocation;
414             return this;
415         }
416 
417         /**
418          * @return Object of {@link SkinTemperatureRecord} without validating the values.
419          * @hide
420          */
421         @NonNull
buildWithoutValidation()422         public SkinTemperatureRecord buildWithoutValidation() {
423             return new SkinTemperatureRecord(
424                     mMetadata,
425                     mStartTime,
426                     mStartZoneOffset,
427                     mEndTime,
428                     mEndZoneOffset,
429                     mBaseline,
430                     mDeltas,
431                     mMeasurementLocation,
432                     true);
433         }
434 
435         /**
436          * @return Object of {@link SkinTemperatureRecord}.
437          */
438         @NonNull
build()439         public SkinTemperatureRecord build() {
440             return new SkinTemperatureRecord(
441                     mMetadata,
442                     mStartTime,
443                     mStartZoneOffset,
444                     mEndTime,
445                     mEndZoneOffset,
446                     mBaseline,
447                     mDeltas,
448                     mMeasurementLocation,
449                     false);
450         }
451     }
452 }
453