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