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 android.annotation.FlaggedApi; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.health.connect.internal.datatypes.PlannedExerciseSessionRecordInternal; 23 24 import java.time.Duration; 25 import java.time.Instant; 26 import java.time.LocalDate; 27 import java.time.LocalTime; 28 import java.time.ZoneId; 29 import java.time.ZoneOffset; 30 import java.util.ArrayList; 31 import java.util.List; 32 import java.util.Objects; 33 import java.util.UUID; 34 import java.util.stream.Collectors; 35 36 /** 37 * Captures a planned exercise session, also commonly referred to as a training plan. 38 * 39 * <p>Each record contains a start time, end time, an exercise type and a list of {@link 40 * PlannedExerciseBlock} which describe the details of the planned session. The start and end times 41 * may be in the future. 42 */ 43 @Identifier(recordIdentifier = RecordTypeIdentifier.RECORD_TYPE_PLANNED_EXERCISE_SESSION) 44 @FlaggedApi("com.android.healthconnect.flags.training_plans") 45 public final class PlannedExerciseSessionRecord extends IntervalRecord { 46 private final Boolean mHasExplicitTime; 47 48 @ExerciseSessionType.ExerciseSessionTypes private final int mExerciseType; 49 50 @Nullable private final CharSequence mTitle; 51 52 @Nullable private final CharSequence mNotes; 53 54 private final List<PlannedExerciseBlock> mBlocks; 55 56 @Nullable private final String mCompletedExerciseSessionId; 57 58 /** 59 * @param metadata Metadata to be associated with the record. See {@link Metadata}. 60 * @param startTime Start time of this planned session. May be in the future. 61 * @param startZoneOffset Zone offset of the user when the planned session should start. 62 * @param endTime End time of this planned session. May be in the future. 63 * @param endZoneOffset Zone offset of the user when the planned session should end. 64 * @param hasExplicitTime Whether an explicit time value has been provided. 65 * @param title The title of this planned session. 66 * @param notes Notes for this planned session. 67 * @param exerciseType The exercise type of this planned s.ession. Allowed values: {@link 68 * ExerciseSessionType.ExerciseSessionTypes }. 69 * @param blocks The {@link PlannedExerciseBlock} that contain the details of this planned 70 * session. 71 * @param completedExerciseSessionId The id of the exercise session that completed this planned 72 * exercise. 73 * @param skipValidation Boolean flag to skip validation of record values. 74 */ PlannedExerciseSessionRecord( @onNull Metadata metadata, @NonNull Instant startTime, @NonNull ZoneOffset startZoneOffset, @NonNull Instant endTime, @NonNull ZoneOffset endZoneOffset, @NonNull Boolean hasExplicitTime, @Nullable CharSequence title, @Nullable CharSequence notes, @NonNull @ExerciseSessionType.ExerciseSessionTypes int exerciseType, @NonNull List<PlannedExerciseBlock> blocks, @Nullable String completedExerciseSessionId, boolean skipValidation)75 private PlannedExerciseSessionRecord( 76 @NonNull Metadata metadata, 77 @NonNull Instant startTime, 78 @NonNull ZoneOffset startZoneOffset, 79 @NonNull Instant endTime, 80 @NonNull ZoneOffset endZoneOffset, 81 @NonNull Boolean hasExplicitTime, 82 @Nullable CharSequence title, 83 @Nullable CharSequence notes, 84 @NonNull @ExerciseSessionType.ExerciseSessionTypes int exerciseType, 85 @NonNull List<PlannedExerciseBlock> blocks, 86 @Nullable String completedExerciseSessionId, 87 boolean skipValidation) { 88 super( 89 metadata, 90 startTime, 91 startZoneOffset, 92 endTime, 93 endZoneOffset, 94 skipValidation, 95 /* enforceFutureTimeRestrictions= */ false); 96 mHasExplicitTime = hasExplicitTime; 97 mTitle = title; 98 mNotes = notes; 99 mExerciseType = exerciseType; 100 mBlocks = blocks; 101 mCompletedExerciseSessionId = completedExerciseSessionId; 102 } 103 104 /** Returns the exercise type of this planned session. */ 105 @ExerciseSessionType.ExerciseSessionTypes getExerciseType()106 public int getExerciseType() { 107 return mExerciseType; 108 } 109 110 /** Returns notes for this planned session. Returns null if it doesn't have notes. */ 111 @Nullable getNotes()112 public CharSequence getNotes() { 113 return mNotes; 114 } 115 116 /** Returns title of this planned session. Returns null if it doesn't have a title. */ 117 @Nullable getTitle()118 public CharSequence getTitle() { 119 return mTitle; 120 } 121 122 /** Returns the start date of the planned session. */ 123 @NonNull getStartDate()124 public LocalDate getStartDate() { 125 return getStartTime().atOffset(getStartZoneOffset()).toLocalDate(); 126 } 127 128 /** Returns the expected duration of the planned session. */ 129 @NonNull getDuration()130 public Duration getDuration() { 131 return Duration.between(getStartTime(), getEndTime()); 132 } 133 134 /** 135 * Returns whether this planned session has an explicit time. If only a date was provided this 136 * will be false. 137 */ hasExplicitTime()138 public boolean hasExplicitTime() { 139 return mHasExplicitTime; 140 } 141 142 /** 143 * Returns the id of exercise session that completed this planned session. Returns null if none 144 * exists. 145 */ 146 @Nullable getCompletedExerciseSessionId()147 public String getCompletedExerciseSessionId() { 148 return mCompletedExerciseSessionId; 149 } 150 151 /** 152 * Returns the exercise blocks for this step. 153 * 154 * @return An unmodifiable list of {@link PlannedExerciseBlock}. 155 */ 156 @NonNull getBlocks()157 public List<PlannedExerciseBlock> getBlocks() { 158 return mBlocks; 159 } 160 161 @Override equals(@ullable Object o)162 public boolean equals(@Nullable Object o) { 163 if (this == o) return true; 164 if (!(o instanceof PlannedExerciseSessionRecord)) return false; 165 if (!super.equals(o)) return false; 166 PlannedExerciseSessionRecord that = (PlannedExerciseSessionRecord) o; 167 return RecordUtils.isEqualNullableCharSequences(this.getNotes(), that.getNotes()) 168 && RecordUtils.isEqualNullableCharSequences(this.getTitle(), that.getTitle()) 169 && this.getExerciseType() == that.getExerciseType() 170 && this.hasExplicitTime() == that.hasExplicitTime() 171 && Objects.equals( 172 this.getCompletedExerciseSessionId(), that.getCompletedExerciseSessionId()) 173 && Objects.equals(this.getBlocks(), that.getBlocks()); 174 } 175 176 @Override hashCode()177 public int hashCode() { 178 return Objects.hash( 179 super.hashCode(), 180 getNotes(), 181 getTitle(), 182 getExerciseType(), 183 hasExplicitTime(), 184 this.getCompletedExerciseSessionId(), 185 this.getBlocks()); 186 } 187 188 /** Builder class for {@link PlannedExerciseSessionRecord}. */ 189 public static final class Builder { 190 @NonNull private Metadata mMetadata; 191 @NonNull private Instant mStartTime; 192 @NonNull private Instant mEndTime; 193 @NonNull private ZoneOffset mStartZoneOffset; 194 @NonNull private ZoneOffset mEndZoneOffset; 195 @ExerciseSessionType.ExerciseSessionTypes private int mExerciseType; 196 @Nullable private CharSequence mNotes; 197 @Nullable private CharSequence mTitle; 198 private Boolean mHasExplicitTime; 199 private final List<PlannedExerciseBlock> mBlocks = new ArrayList<>(); 200 @Nullable private String mCompletedExerciseSessionId; 201 202 /** 203 * @param metadata Metadata to be associated with the record. See {@link Metadata}. 204 * @param exerciseType Type of exercise (e.g. walking, swimming). Allowed values: {@link 205 * ExerciseSessionType}. 206 * @param startTime Expected start time of this planned session. 207 * @param endTime Expected end time of this planned session. 208 */ Builder( @onNull Metadata metadata, @ExerciseSessionType.ExerciseSessionTypes int exerciseType, @NonNull Instant startTime, @NonNull Instant endTime)209 public Builder( 210 @NonNull Metadata metadata, 211 @ExerciseSessionType.ExerciseSessionTypes int exerciseType, 212 @NonNull Instant startTime, 213 @NonNull Instant endTime) { 214 Objects.requireNonNull(metadata); 215 Objects.requireNonNull(startTime); 216 Objects.requireNonNull(endTime); 217 mMetadata = metadata; 218 mExerciseType = exerciseType; 219 mHasExplicitTime = true; 220 mStartTime = startTime; 221 mStartZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(mStartTime); 222 mEndTime = endTime; 223 mEndZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(mEndTime); 224 } 225 226 /** 227 * @param metadata Metadata to be associated with the record. See {@link Metadata}. 228 * @param exerciseType Type of exercise (e.g. walking, swimming). Required field. Allowed 229 * values: {@link ExerciseSessionType}. 230 * @param startDate The start date of this planned session. The underlying time of the 231 * session will be set to noon of the specified day. The end time will be determined by 232 * adding the duration to this. 233 * @param duration The expected duration of the planned session. 234 */ Builder( @onNull Metadata metadata, @ExerciseSessionType.ExerciseSessionTypes int exerciseType, @NonNull LocalDate startDate, @NonNull Duration duration)235 public Builder( 236 @NonNull Metadata metadata, 237 @ExerciseSessionType.ExerciseSessionTypes int exerciseType, 238 @NonNull LocalDate startDate, 239 @NonNull Duration duration) { 240 Objects.requireNonNull(metadata); 241 Objects.requireNonNull(startDate); 242 Objects.requireNonNull(duration); 243 mMetadata = metadata; 244 mExerciseType = exerciseType; 245 mHasExplicitTime = false; 246 mStartTime = 247 startDate.atTime(LocalTime.NOON).atZone(ZoneId.systemDefault()).toInstant(); 248 mStartZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(mStartTime); 249 mEndTime = mStartTime.plus(duration); 250 mEndZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(mEndTime); 251 } 252 253 /** Set the metadata for the record. */ 254 @NonNull setMetadata(@onNull Metadata metadata)255 public Builder setMetadata(@NonNull Metadata metadata) { 256 Objects.requireNonNull(metadata); 257 this.mMetadata = metadata; 258 return this; 259 } 260 261 /** Sets the exercise type. */ 262 @NonNull setExerciseType(@xerciseSessionType.ExerciseSessionTypes int exerciseType)263 public Builder setExerciseType(@ExerciseSessionType.ExerciseSessionTypes int exerciseType) { 264 this.mExerciseType = exerciseType; 265 return this; 266 } 267 268 /** Sets the planned start time of the session. */ 269 @NonNull setStartTime(@onNull Instant startTime)270 public Builder setStartTime(@NonNull Instant startTime) { 271 Objects.requireNonNull(startTime); 272 this.mStartTime = startTime; 273 mStartZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(mStartTime); 274 return this; 275 } 276 277 /** Sets the planned end time of the session. */ 278 @NonNull setEndTime(@onNull Instant endTime)279 public Builder setEndTime(@NonNull Instant endTime) { 280 Objects.requireNonNull(endTime); 281 this.mEndTime = endTime; 282 mEndZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(mEndTime); 283 return this; 284 } 285 286 /** Sets the zone offset of when the workout should start. */ 287 @NonNull setStartZoneOffset(@onNull ZoneOffset startZoneOffset)288 public Builder setStartZoneOffset(@NonNull ZoneOffset startZoneOffset) { 289 Objects.requireNonNull(startZoneOffset); 290 mStartZoneOffset = startZoneOffset; 291 return this; 292 } 293 294 /** Sets the zone offset of when the workout should end. */ 295 @NonNull setEndZoneOffset(@onNull ZoneOffset endZoneOffset)296 public Builder setEndZoneOffset(@NonNull ZoneOffset endZoneOffset) { 297 Objects.requireNonNull(endZoneOffset); 298 mEndZoneOffset = endZoneOffset; 299 return this; 300 } 301 302 /** Sets the start zone offset of this record to system default. */ 303 @NonNull clearStartZoneOffset()304 public Builder clearStartZoneOffset() { 305 mStartZoneOffset = RecordUtils.getDefaultZoneOffset(); 306 return this; 307 } 308 309 /** Sets the start zone offset of this record to system default. */ 310 @NonNull clearEndZoneOffset()311 public Builder clearEndZoneOffset() { 312 mEndZoneOffset = RecordUtils.getDefaultZoneOffset(); 313 return this; 314 } 315 316 /** 317 * Sets notes for this activity. 318 * 319 * @param notes Notes for this activity. 320 */ 321 @NonNull setNotes(@ullable CharSequence notes)322 public Builder setNotes(@Nullable CharSequence notes) { 323 mNotes = notes; 324 return this; 325 } 326 327 /** 328 * Sets a title of this planned session. 329 * 330 * @param title Title of this activity. 331 */ 332 @NonNull setTitle(@ullable CharSequence title)333 public Builder setTitle(@Nullable CharSequence title) { 334 mTitle = title; 335 return this; 336 } 337 338 /** 339 * Adds a block to this planned session.. 340 * 341 * @param block An {@link PlannedExerciseBlock} to add to this planned session.. 342 */ 343 @NonNull addBlock(@onNull PlannedExerciseBlock block)344 public Builder addBlock(@NonNull PlannedExerciseBlock block) { 345 Objects.requireNonNull(block); 346 mBlocks.add(block); 347 return this; 348 } 349 350 /** 351 * Sets the blocks of this planned session. 352 * 353 * @param blocks A list of {@link PlannedExerciseBlock} to set for this planned session. 354 */ 355 @NonNull setBlocks(@onNull List<PlannedExerciseBlock> blocks)356 public Builder setBlocks(@NonNull List<PlannedExerciseBlock> blocks) { 357 Objects.requireNonNull(blocks); 358 mBlocks.clear(); 359 mBlocks.addAll(blocks); 360 return this; 361 } 362 363 /** Clears the blocks of this planned session. */ 364 @NonNull clearBlocks()365 public Builder clearBlocks() { 366 mBlocks.clear(); 367 return this; 368 } 369 370 /** 371 * Set exercise session ID of the workout that completed this planned session. 372 * 373 * @hide 374 */ 375 @NonNull setCompletedExerciseSessionId(@ullable String id)376 public Builder setCompletedExerciseSessionId(@Nullable String id) { 377 this.mCompletedExerciseSessionId = id; 378 return this; 379 } 380 381 /** 382 * @return Object of {@link PlannedExerciseSessionRecord} without validating the values. 383 * @hide 384 */ 385 @NonNull buildWithoutValidation()386 public PlannedExerciseSessionRecord buildWithoutValidation() { 387 return new PlannedExerciseSessionRecord( 388 mMetadata, 389 mStartTime, 390 mStartZoneOffset, 391 mEndTime, 392 mEndZoneOffset, 393 mHasExplicitTime, 394 mTitle, 395 mNotes, 396 mExerciseType, 397 List.copyOf(mBlocks), 398 mCompletedExerciseSessionId, 399 true); 400 } 401 402 /** Returns {@link PlannedExerciseSessionRecord}. */ 403 @NonNull build()404 public PlannedExerciseSessionRecord build() { 405 if (!ExerciseSessionType.isKnownSessionType(mExerciseType)) { 406 throw new IllegalArgumentException("Invalid exercise session type"); 407 } 408 return new PlannedExerciseSessionRecord( 409 mMetadata, 410 mStartTime, 411 mStartZoneOffset, 412 mEndTime, 413 mEndZoneOffset, 414 mHasExplicitTime, 415 mTitle, 416 mNotes, 417 mExerciseType, 418 List.copyOf(mBlocks), 419 mCompletedExerciseSessionId, 420 false); 421 } 422 } 423 424 /** @hide */ 425 @Override toRecordInternal()426 public PlannedExerciseSessionRecordInternal toRecordInternal() { 427 PlannedExerciseSessionRecordInternal recordInternal = 428 (PlannedExerciseSessionRecordInternal) 429 new PlannedExerciseSessionRecordInternal() 430 .setUuid(getMetadata().getId()) 431 .setPackageName(getMetadata().getDataOrigin().getPackageName()) 432 .setLastModifiedTime( 433 getMetadata().getLastModifiedTime().toEpochMilli()) 434 .setClientRecordId(getMetadata().getClientRecordId()) 435 .setClientRecordVersion(getMetadata().getClientRecordVersion()) 436 .setManufacturer(getMetadata().getDevice().getManufacturer()) 437 .setModel(getMetadata().getDevice().getModel()) 438 .setDeviceType(getMetadata().getDevice().getType()) 439 .setRecordingMethod(getMetadata().getRecordingMethod()); 440 recordInternal.setStartTime(getStartTime().toEpochMilli()); 441 recordInternal.setEndTime(getEndTime().toEpochMilli()); 442 recordInternal.setStartZoneOffset(getStartZoneOffset().getTotalSeconds()); 443 recordInternal.setEndZoneOffset(getEndZoneOffset().getTotalSeconds()); 444 if (getNotes() != null) { 445 recordInternal.setNotes(getNotes().toString()); 446 } 447 if (getTitle() != null) { 448 recordInternal.setTitle(getTitle().toString()); 449 } 450 recordInternal.setExerciseType(getExerciseType()); 451 recordInternal.setHasExplicitTime(hasExplicitTime()); 452 // Although not possible to set this via public API, internally we may convert from internal 453 // representation to external, then back to internal. Thus, we need to preserve this value 454 // during a round trip. 455 if (getCompletedExerciseSessionId() != null) { 456 recordInternal.setCompletedExerciseSessionId( 457 UUID.fromString(getCompletedExerciseSessionId())); 458 } 459 recordInternal.setExerciseBlocks( 460 getBlocks().stream().map(it -> it.toInternalObject()).collect(Collectors.toList())); 461 return recordInternal; 462 } 463 } 464