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