• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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