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_SLEEP_SESSION; 19 import static android.health.connect.datatypes.RecordUtils.isEqualNullableCharSequences; 20 import static android.health.connect.datatypes.validation.ValidationUtils.sortAndValidateTimeIntervalHolders; 21 import static android.health.connect.datatypes.validation.ValidationUtils.validateIntDefValue; 22 23 import android.annotation.IntDef; 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.health.connect.internal.datatypes.SleepSessionRecordInternal; 27 import android.health.connect.internal.datatypes.SleepStageInternal; 28 29 import java.lang.annotation.Retention; 30 import java.lang.annotation.RetentionPolicy; 31 import java.time.Instant; 32 import java.time.ZoneOffset; 33 import java.util.ArrayList; 34 import java.util.Collections; 35 import java.util.List; 36 import java.util.Objects; 37 import java.util.Set; 38 import java.util.stream.Collectors; 39 40 /** 41 * Captures the user's sleep length and its stages. Each record represents a time interval for a 42 * full sleep session. 43 * 44 * <p>All {@link Stage} time intervals should fall within the sleep session interval. Time intervals 45 * for stages don't need to be continuous but shouldn't overlap. 46 */ 47 @Identifier(recordIdentifier = RecordTypeIdentifier.RECORD_TYPE_SLEEP_SESSION) 48 public final class SleepSessionRecord extends IntervalRecord { 49 50 /** 51 * Metric identifier to retrieve total sleep session duration using aggregate APIs in {@link 52 * android.health.connect.HealthConnectManager}. Calculated in milliseconds. 53 */ 54 @NonNull 55 public static final AggregationType<Long> SLEEP_DURATION_TOTAL = 56 new AggregationType<>( 57 AggregationType.AggregationTypeIdentifier.SLEEP_SESSION_DURATION_TOTAL, 58 AggregationType.SUM, 59 RECORD_TYPE_SLEEP_SESSION, 60 Long.class); 61 62 private final List<Stage> mStages; 63 private final CharSequence mNotes; 64 private final CharSequence mTitle; 65 66 /** 67 * Builds {@link SleepSessionRecord} instance 68 * 69 * @param metadata Metadata to be associated with the record. See {@link Metadata}. 70 * @param startTime Start time of this activity 71 * @param startZoneOffset Zone offset of the user when the session started 72 * @param endTime End time of this activity 73 * @param endZoneOffset Zone offset of the user when the session finished 74 * @param stages list of {@link Stage} of the sleep sessions. 75 * @param notes Additional notes for the session. Optional field. 76 * @param title Title of the session. Optional field. 77 * @param skipValidation Boolean flag to skip validation of record values. 78 */ 79 @SuppressWarnings({"unchecked", "NullAway"}) SleepSessionRecord( @onNull Metadata metadata, @NonNull Instant startTime, @NonNull ZoneOffset startZoneOffset, @NonNull Instant endTime, @NonNull ZoneOffset endZoneOffset, @NonNull List<Stage> stages, @Nullable CharSequence notes, @Nullable CharSequence title, boolean skipValidation)80 private SleepSessionRecord( 81 @NonNull Metadata metadata, 82 @NonNull Instant startTime, 83 @NonNull ZoneOffset startZoneOffset, 84 @NonNull Instant endTime, 85 @NonNull ZoneOffset endZoneOffset, 86 @NonNull List<Stage> stages, 87 @Nullable CharSequence notes, 88 @Nullable CharSequence title, 89 boolean skipValidation) { 90 super( 91 metadata, 92 startTime, 93 startZoneOffset, 94 endTime, 95 endZoneOffset, 96 skipValidation, 97 /* enforceFutureTimeRestrictions= */ true); 98 Objects.requireNonNull(stages); 99 mStages = 100 Collections.unmodifiableList( 101 (List<Stage>) 102 sortAndValidateTimeIntervalHolders(startTime, endTime, stages)); 103 mNotes = notes; 104 mTitle = title; 105 } 106 107 /** Returns notes for the sleep session. Returns null if no notes was specified. */ 108 @Nullable getNotes()109 public CharSequence getNotes() { 110 return mNotes; 111 } 112 113 /** Returns title of the sleep session. Returns null if no notes was specified. */ 114 @Nullable getTitle()115 public CharSequence getTitle() { 116 return mTitle; 117 } 118 119 /** Returns stages of the sleep session. */ 120 @NonNull getStages()121 public List<Stage> getStages() { 122 return mStages; 123 } 124 125 @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression 126 @Override equals(Object o)127 public boolean equals(Object o) { 128 if (this == o) return true; 129 if (!(o instanceof SleepSessionRecord)) return false; 130 if (!super.equals(o)) return false; 131 SleepSessionRecord that = (SleepSessionRecord) o; 132 return isEqualNullableCharSequences(getNotes(), that.getNotes()) 133 && isEqualNullableCharSequences(getTitle(), that.getTitle()) 134 && Objects.equals(getStages(), that.getStages()); 135 } 136 137 @Override hashCode()138 public int hashCode() { 139 return Objects.hash(super.hashCode(), getNotes(), getTitle(), getStages()); 140 } 141 142 /** 143 * Captures the user's length and type of sleep. Each record represents a time interval for a 144 * stage of sleep. 145 * 146 * <p>The start time of the record represents the start and end time of the sleep stage and 147 * always need to be included. 148 */ 149 public static class Stage implements TimeInterval.TimeIntervalHolder { 150 @NonNull private final TimeInterval mInterval; 151 @StageType.StageTypes private final int mStageType; 152 153 /** 154 * Builds {@link Stage} instance 155 * 156 * @param startTime start time of the stage 157 * @param endTime end time of the stage. Must not be earlier than start time. 158 * @param stageType type of the stage. One of {@link StageType} 159 */ Stage( @onNull Instant startTime, @NonNull Instant endTime, @StageType.StageTypes int stageType)160 public Stage( 161 @NonNull Instant startTime, 162 @NonNull Instant endTime, 163 @StageType.StageTypes int stageType) { 164 validateIntDefValue(stageType, StageType.VALID_TYPES, StageType.class.getSimpleName()); 165 this.mInterval = new TimeInterval(startTime, endTime); 166 this.mStageType = stageType; 167 } 168 169 /** Returns start time of this stage. */ 170 @NonNull getStartTime()171 public Instant getStartTime() { 172 return mInterval.getStartTime(); 173 } 174 175 /** Returns end time of this stage. */ 176 @NonNull getEndTime()177 public Instant getEndTime() { 178 return mInterval.getEndTime(); 179 } 180 181 /** Returns stage type. */ 182 @StageType.StageTypes getType()183 public int getType() { 184 return mStageType; 185 } 186 187 /** @hide */ 188 @Override getInterval()189 public TimeInterval getInterval() { 190 return mInterval; 191 } 192 193 @Override equals(Object o)194 public boolean equals(Object o) { 195 if (this == o) return true; 196 if (!(o instanceof Stage)) return false; 197 Stage that = (Stage) o; 198 return getType() == that.getType() 199 && getStartTime().toEpochMilli() == that.getStartTime().toEpochMilli() 200 && getEndTime().toEpochMilli() == that.getEndTime().toEpochMilli(); 201 } 202 203 @Override hashCode()204 public int hashCode() { 205 return Objects.hash(getStartTime(), getEndTime(), mStageType); 206 } 207 208 /** @hide */ toInternalStage()209 public SleepStageInternal toInternalStage() { 210 return new SleepStageInternal() 211 .setStartTime(getStartTime().toEpochMilli()) 212 .setEndTime(getEndTime().toEpochMilli()) 213 .setStageType(getType()); 214 } 215 } 216 217 /** Identifier for sleeping stage, as returned by {@link Stage#getType()}. */ 218 public static final class StageType { 219 /** Use this type if the stage of sleep is unknown. */ 220 public static final int STAGE_TYPE_UNKNOWN = 0; 221 222 /** 223 * The user is awake and either known to be in bed, or it is unknown whether they are in bed 224 * or not. 225 */ 226 public static final int STAGE_TYPE_AWAKE = 1; 227 228 /** The user is asleep but the particular stage of sleep (light, deep or REM) is unknown. */ 229 public static final int STAGE_TYPE_SLEEPING = 2; 230 231 /** The user is out of bed and assumed to be awake. */ 232 public static final int STAGE_TYPE_AWAKE_OUT_OF_BED = 3; 233 234 /** The user is in a light sleep stage. */ 235 public static final int STAGE_TYPE_SLEEPING_LIGHT = 4; 236 237 /** The user is in a deep sleep stage. */ 238 public static final int STAGE_TYPE_SLEEPING_DEEP = 5; 239 240 /** The user is in a REM sleep stage. */ 241 public static final int STAGE_TYPE_SLEEPING_REM = 6; 242 243 /** The user is awake and in bed. */ 244 public static final int STAGE_TYPE_AWAKE_IN_BED = 7; 245 246 /** 247 * Valid set of values for this IntDef. Update this set when add new type or deprecate 248 * existing type. 249 * 250 * @hide 251 */ 252 public static final Set<Integer> VALID_TYPES = 253 Set.of( 254 STAGE_TYPE_UNKNOWN, 255 STAGE_TYPE_AWAKE, 256 STAGE_TYPE_SLEEPING, 257 STAGE_TYPE_AWAKE_OUT_OF_BED, 258 STAGE_TYPE_SLEEPING_LIGHT, 259 STAGE_TYPE_SLEEPING_DEEP, 260 STAGE_TYPE_SLEEPING_REM, 261 STAGE_TYPE_AWAKE_IN_BED); 262 StageType()263 private StageType() {} 264 265 /** @hide */ 266 @IntDef({ 267 STAGE_TYPE_UNKNOWN, 268 STAGE_TYPE_AWAKE, 269 STAGE_TYPE_SLEEPING, 270 STAGE_TYPE_AWAKE_OUT_OF_BED, 271 STAGE_TYPE_SLEEPING_LIGHT, 272 STAGE_TYPE_SLEEPING_DEEP, 273 STAGE_TYPE_SLEEPING_REM, 274 STAGE_TYPE_AWAKE_IN_BED 275 }) 276 @Retention(RetentionPolicy.SOURCE) 277 public @interface StageTypes {} 278 279 /** 280 * Sleep stage types which are excluded from sleep session duration. 281 * 282 * @hide 283 */ 284 public static final List<Integer> DURATION_EXCLUDE_TYPES = 285 List.of(STAGE_TYPE_AWAKE, STAGE_TYPE_AWAKE_OUT_OF_BED, STAGE_TYPE_AWAKE_IN_BED); 286 } 287 288 /** Builder class for {@link SleepSessionRecord} */ 289 public static final class Builder { 290 private final Metadata mMetadata; 291 private final Instant mStartTime; 292 private final Instant mEndTime; 293 private final List<Stage> mStages; 294 private ZoneOffset mStartZoneOffset; 295 private ZoneOffset mEndZoneOffset; 296 private CharSequence mNotes; 297 private CharSequence mTitle; 298 299 /** 300 * @param metadata Metadata to be associated with the record. See {@link Metadata}. 301 * @param startTime Start time of this sleep session 302 * @param endTime End time of this sleep session 303 */ 304 @SuppressWarnings("NullAway.Init") // TODO(b/317029272): fix this suppression Builder( @onNull Metadata metadata, @NonNull Instant startTime, @NonNull Instant endTime)305 public Builder( 306 @NonNull Metadata metadata, @NonNull Instant startTime, @NonNull Instant endTime) { 307 Objects.requireNonNull(metadata); 308 Objects.requireNonNull(startTime); 309 Objects.requireNonNull(endTime); 310 mMetadata = metadata; 311 mStartTime = startTime; 312 mEndTime = endTime; 313 mStages = new ArrayList<>(); 314 mStartZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(startTime); 315 mEndZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(endTime); 316 } 317 318 /** Sets the zone offset of the user when the activity started */ 319 @NonNull setStartZoneOffset(@onNull ZoneOffset startZoneOffset)320 public Builder setStartZoneOffset(@NonNull ZoneOffset startZoneOffset) { 321 Objects.requireNonNull(startZoneOffset); 322 323 mStartZoneOffset = startZoneOffset; 324 return this; 325 } 326 327 /** Sets the zone offset of the user when the activity ended */ 328 @NonNull setEndZoneOffset(@onNull ZoneOffset endZoneOffset)329 public Builder setEndZoneOffset(@NonNull ZoneOffset endZoneOffset) { 330 Objects.requireNonNull(endZoneOffset); 331 mEndZoneOffset = endZoneOffset; 332 return this; 333 } 334 335 /** Sets the start zone offset of this record to system default. */ 336 @NonNull clearStartZoneOffset()337 public Builder clearStartZoneOffset() { 338 mStartZoneOffset = RecordUtils.getDefaultZoneOffset(); 339 return this; 340 } 341 342 /** Sets the start zone offset of this record to system default. */ 343 @NonNull clearEndZoneOffset()344 public Builder clearEndZoneOffset() { 345 mEndZoneOffset = RecordUtils.getDefaultZoneOffset(); 346 return this; 347 } 348 349 /** 350 * Sets notes for this activity 351 * 352 * @param notes Additional notes for the session. Optional field. 353 */ 354 @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression 355 @NonNull setNotes(@ullable CharSequence notes)356 public Builder setNotes(@Nullable CharSequence notes) { 357 mNotes = notes; 358 return this; 359 } 360 361 /** 362 * Sets a title of this activity 363 * 364 * @param title Title of the session. Optional field. 365 */ 366 @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression 367 @NonNull setTitle(@ullable CharSequence title)368 public Builder setTitle(@Nullable CharSequence title) { 369 mTitle = title; 370 return this; 371 } 372 373 /** 374 * Set stages to this sleep session. Returns Object with updated stages. 375 * 376 * @param stages list of stages to set 377 */ 378 @NonNull setStages(@onNull List<Stage> stages)379 public Builder setStages(@NonNull List<Stage> stages) { 380 Objects.requireNonNull(stages); 381 mStages.clear(); 382 mStages.addAll(stages); 383 return this; 384 } 385 386 /** 387 * @return Object of {@link SleepSessionRecord} without validating the values. 388 * @hide 389 */ 390 @NonNull buildWithoutValidation()391 public SleepSessionRecord buildWithoutValidation() { 392 return new SleepSessionRecord( 393 mMetadata, 394 mStartTime, 395 mStartZoneOffset, 396 mEndTime, 397 mEndZoneOffset, 398 mStages, 399 mNotes, 400 mTitle, 401 true); 402 } 403 404 /** Returns {@link SleepSessionRecord} */ 405 @NonNull build()406 public SleepSessionRecord build() { 407 return new SleepSessionRecord( 408 mMetadata, 409 mStartTime, 410 mStartZoneOffset, 411 mEndTime, 412 mEndZoneOffset, 413 mStages, 414 mNotes, 415 mTitle, 416 false); 417 } 418 } 419 420 /** @hide */ 421 @Override toRecordInternal()422 public SleepSessionRecordInternal toRecordInternal() { 423 SleepSessionRecordInternal recordInternal = 424 (SleepSessionRecordInternal) 425 new SleepSessionRecordInternal() 426 .setUuid(getMetadata().getId()) 427 .setPackageName(getMetadata().getDataOrigin().getPackageName()) 428 .setLastModifiedTime( 429 getMetadata().getLastModifiedTime().toEpochMilli()) 430 .setClientRecordId(getMetadata().getClientRecordId()) 431 .setClientRecordVersion(getMetadata().getClientRecordVersion()) 432 .setManufacturer(getMetadata().getDevice().getManufacturer()) 433 .setModel(getMetadata().getDevice().getModel()) 434 .setDeviceType(getMetadata().getDevice().getType()) 435 .setRecordingMethod(getMetadata().getRecordingMethod()); 436 recordInternal.setStartTime(getStartTime().toEpochMilli()); 437 recordInternal.setEndTime(getEndTime().toEpochMilli()); 438 recordInternal.setStartZoneOffset(getStartZoneOffset().getTotalSeconds()); 439 recordInternal.setEndZoneOffset(getEndZoneOffset().getTotalSeconds()); 440 recordInternal.setSleepStages( 441 getStages().stream().map(Stage::toInternalStage).collect(Collectors.toList())); 442 443 if (getNotes() != null) { 444 recordInternal.setNotes(getNotes().toString()); 445 } 446 447 if (getTitle() != null) { 448 recordInternal.setTitle(getTitle().toString()); 449 } 450 return recordInternal; 451 } 452 } 453