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