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