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