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