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