• 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 
17 package android.health.connect.datatypes;
18 
19 import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_EXERCISE_SESSION;
20 import static android.health.connect.datatypes.validation.ValidationUtils.sortAndValidateTimeIntervalHolders;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.health.connect.datatypes.validation.ExerciseSessionTypesValidation;
25 import android.health.connect.internal.datatypes.ExerciseSessionRecordInternal;
26 
27 import java.time.Instant;
28 import java.time.ZoneOffset;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.List;
32 import java.util.Objects;
33 import java.util.stream.Collectors;
34 
35 /**
36  * Captures exercise or a sequence of exercises. This can be a playing game like football or a
37  * sequence of fitness exercises.
38  *
39  * <p>Each record needs a start time, end time and session type. In addition, each record has two
40  * optional independent lists of time intervals: {@link ExerciseSegment} represents particular
41  * exercise within session, {@link ExerciseLap} represents a lap time within session.
42  */
43 @Identifier(recordIdentifier = RECORD_TYPE_EXERCISE_SESSION)
44 public final class ExerciseSessionRecord extends IntervalRecord {
45 
46     /**
47      * Metric identifier to retrieve total exercise session duration using aggregate APIs in {@link
48      * android.health.connect.HealthConnectManager}. Calculated in milliseconds.
49      */
50     @NonNull
51     public static final AggregationType<Long> EXERCISE_DURATION_TOTAL =
52             new AggregationType<>(
53                     AggregationType.AggregationTypeIdentifier.EXERCISE_SESSION_DURATION_TOTAL,
54                     AggregationType.SUM,
55                     RECORD_TYPE_EXERCISE_SESSION,
56                     Long.class);
57 
58     private final int mExerciseType;
59 
60     private final CharSequence mNotes;
61     private final CharSequence mTitle;
62     private final ExerciseRoute mRoute;
63 
64     // Represents if the route is recorded for this session, even if mRoute is null.
65     private final boolean mHasRoute;
66 
67     private final List<ExerciseSegment> mSegments;
68     private final List<ExerciseLap> mLaps;
69 
70     /**
71      * @param metadata Metadata to be associated with the record. See {@link Metadata}.
72      * @param startTime Start time of this activity
73      * @param startZoneOffset Zone offset of the user when the activity started
74      * @param endTime End time of this activity
75      * @param endZoneOffset Zone offset of the user when the activity finished
76      * @param notes Notes for this activity
77      * @param exerciseType Type of exercise (e.g. walking, swimming). Required field. Allowed
78      *     values: {@link ExerciseSessionType.ExerciseSessionTypes }
79      * @param title Title of this activity
80      * @param skipValidation Boolean flag to skip validation of record values.
81      */
82     @SuppressWarnings("unchecked")
ExerciseSessionRecord( @onNull Metadata metadata, @NonNull Instant startTime, @NonNull ZoneOffset startZoneOffset, @NonNull Instant endTime, @NonNull ZoneOffset endZoneOffset, @Nullable CharSequence notes, @NonNull @ExerciseSessionType.ExerciseSessionTypes int exerciseType, @Nullable CharSequence title, @Nullable ExerciseRoute route, boolean hasRoute, @NonNull List<ExerciseSegment> segments, @NonNull List<ExerciseLap> laps, boolean skipValidation)83     private ExerciseSessionRecord(
84             @NonNull Metadata metadata,
85             @NonNull Instant startTime,
86             @NonNull ZoneOffset startZoneOffset,
87             @NonNull Instant endTime,
88             @NonNull ZoneOffset endZoneOffset,
89             @Nullable CharSequence notes,
90             @NonNull @ExerciseSessionType.ExerciseSessionTypes int exerciseType,
91             @Nullable CharSequence title,
92             @Nullable ExerciseRoute route,
93             boolean hasRoute,
94             @NonNull List<ExerciseSegment> segments,
95             @NonNull List<ExerciseLap> laps,
96             boolean skipValidation) {
97         super(metadata, startTime, startZoneOffset, endTime, endZoneOffset, skipValidation);
98         mNotes = notes;
99         mExerciseType = exerciseType;
100         mTitle = title;
101         if (route != null && !hasRoute) {
102             throw new IllegalArgumentException("HasRoute must be true if the route is not null");
103         }
104         mRoute = route;
105         if (!skipValidation) {
106             ExerciseSessionTypesValidation.validateExerciseRouteTimestamps(
107                     startTime, endTime, route);
108         }
109         mHasRoute = hasRoute;
110         mSegments =
111                 Collections.unmodifiableList(
112                         (List<ExerciseSegment>)
113                                 sortAndValidateTimeIntervalHolders(startTime, endTime, segments));
114         if (!skipValidation) {
115             ExerciseSessionTypesValidation.validateSessionAndSegmentsTypes(exerciseType, mSegments);
116         }
117         mLaps =
118                 Collections.unmodifiableList(
119                         (List<ExerciseLap>)
120                                 sortAndValidateTimeIntervalHolders(startTime, endTime, laps));
121     }
122 
123     /** Returns exerciseType of this session. */
124     @ExerciseSessionType.ExerciseSessionTypes
getExerciseType()125     public int getExerciseType() {
126         return mExerciseType;
127     }
128 
129     /** Returns notes for this activity. Returns null if the session doesn't have notes. */
130     @Nullable
getNotes()131     public CharSequence getNotes() {
132         return mNotes;
133     }
134 
135     /** Returns title of this session. Returns null if the session doesn't have title. */
136     @Nullable
getTitle()137     public CharSequence getTitle() {
138         return mTitle;
139     }
140 
141     /** Returns route of this session. Returns null if the session doesn't have route. */
142     @Nullable
getRoute()143     public ExerciseRoute getRoute() {
144         return mRoute;
145     }
146 
147     /**
148      * Returns segments of this session. Returns empty list if the session doesn't have exercise
149      * segments.
150      */
151     @NonNull
getSegments()152     public List<ExerciseSegment> getSegments() {
153         return mSegments;
154     }
155 
156     /**
157      * Returns laps of this session. Returns empty list if the session doesn't have exercise laps.
158      */
159     @NonNull
getLaps()160     public List<ExerciseLap> getLaps() {
161         return mLaps;
162     }
163 
164     /** Returns if this session has recorded route. */
165     @NonNull
hasRoute()166     public boolean hasRoute() {
167         return mHasRoute;
168     }
169 
170     @Override
equals(Object o)171     public boolean equals(Object o) {
172         if (this == o) return true;
173         if (!(o instanceof ExerciseSessionRecord)) return false;
174         if (!super.equals(o)) return false;
175         ExerciseSessionRecord that = (ExerciseSessionRecord) o;
176         return getExerciseType() == that.getExerciseType()
177                 && RecordUtils.isEqualNullableCharSequences(getNotes(), that.getNotes())
178                 && RecordUtils.isEqualNullableCharSequences(getTitle(), that.getTitle())
179                 && Objects.equals(getRoute(), that.getRoute())
180                 && Objects.equals(getSegments(), that.getSegments())
181                 && Objects.equals(getLaps(), that.getLaps());
182     }
183 
184     @Override
hashCode()185     public int hashCode() {
186         return Objects.hash(
187                 super.hashCode(),
188                 getExerciseType(),
189                 getNotes(),
190                 getTitle(),
191                 getRoute(),
192                 getSegments(),
193                 getLaps());
194     }
195 
196     /** Builder class for {@link ExerciseSessionRecord} */
197     public static final class Builder {
198         private final Metadata mMetadata;
199         private final Instant mStartTime;
200         private final Instant mEndTime;
201         private ZoneOffset mStartZoneOffset;
202         private ZoneOffset mEndZoneOffset;
203         private final int mExerciseType;
204         private CharSequence mNotes;
205         private CharSequence mTitle;
206         private ExerciseRoute mRoute;
207         private final List<ExerciseSegment> mSegments;
208         private final List<ExerciseLap> mLaps;
209         private boolean mHasRoute;
210 
211         /**
212          * @param metadata Metadata to be associated with the record. See {@link Metadata}.
213          * @param startTime Start time of this activity
214          * @param endTime End time of this activity
215          * @param exerciseType Type of exercise (e.g. walking, swimming). Required field. Allowed
216          *     values: {@link ExerciseSessionType}
217          */
Builder( @onNull Metadata metadata, @NonNull Instant startTime, @NonNull Instant endTime, @ExerciseSessionType.ExerciseSessionTypes int exerciseType)218         public Builder(
219                 @NonNull Metadata metadata,
220                 @NonNull Instant startTime,
221                 @NonNull Instant endTime,
222                 @ExerciseSessionType.ExerciseSessionTypes int exerciseType) {
223             Objects.requireNonNull(metadata);
224             Objects.requireNonNull(startTime);
225             Objects.requireNonNull(endTime);
226             mMetadata = metadata;
227             mStartTime = startTime;
228             mEndTime = endTime;
229             mExerciseType = exerciseType;
230             mSegments = new ArrayList<>();
231             mLaps = new ArrayList<>();
232             mStartZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(startTime);
233             mEndZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(endTime);
234         }
235 
236         /** Sets the zone offset of the user when the session started */
237         @NonNull
setStartZoneOffset(@onNull ZoneOffset startZoneOffset)238         public Builder setStartZoneOffset(@NonNull ZoneOffset startZoneOffset) {
239             Objects.requireNonNull(startZoneOffset);
240 
241             mStartZoneOffset = startZoneOffset;
242             return this;
243         }
244 
245         /** Sets the zone offset of the user when the session ended */
246         @NonNull
setEndZoneOffset(@onNull ZoneOffset endZoneOffset)247         public Builder setEndZoneOffset(@NonNull ZoneOffset endZoneOffset) {
248             Objects.requireNonNull(endZoneOffset);
249 
250             mEndZoneOffset = endZoneOffset;
251             return this;
252         }
253 
254         /** Sets the start zone offset of this record to system default. */
255         @NonNull
clearStartZoneOffset()256         public Builder clearStartZoneOffset() {
257             mStartZoneOffset = RecordUtils.getDefaultZoneOffset();
258             return this;
259         }
260 
261         /** Sets the start zone offset of this record to system default. */
262         @NonNull
clearEndZoneOffset()263         public Builder clearEndZoneOffset() {
264             mEndZoneOffset = RecordUtils.getDefaultZoneOffset();
265             return this;
266         }
267 
268         /**
269          * Sets notes for this activity
270          *
271          * @param notes Notes for this activity
272          */
273         @NonNull
setNotes(@ullable CharSequence notes)274         public Builder setNotes(@Nullable CharSequence notes) {
275             mNotes = notes;
276             return this;
277         }
278 
279         /**
280          * Sets a title of this activity
281          *
282          * @param title Title of this activity
283          */
284         @NonNull
setTitle(@ullable CharSequence title)285         public Builder setTitle(@Nullable CharSequence title) {
286             mTitle = title;
287             return this;
288         }
289 
290         /**
291          * Sets route for this activity
292          *
293          * @param route ExerciseRoute for this activity
294          */
295         @NonNull
setRoute(@ullable ExerciseRoute route)296         public Builder setRoute(@Nullable ExerciseRoute route) {
297             mRoute = route;
298             mHasRoute = (route != null);
299             return this;
300         }
301 
302         /**
303          * Sets segments for this session.
304          *
305          * @param laps list of {@link ExerciseLap} of this session
306          */
307         @NonNull
setLaps(@onNull List<ExerciseLap> laps)308         public Builder setLaps(@NonNull List<ExerciseLap> laps) {
309             Objects.requireNonNull(laps);
310             mLaps.clear();
311             mLaps.addAll(laps);
312             return this;
313         }
314 
315         /**
316          * Sets segments for this session.
317          *
318          * @param segments list of {@link ExerciseSegment} of this session
319          */
320         @NonNull
setSegments(@onNull List<ExerciseSegment> segments)321         public Builder setSegments(@NonNull List<ExerciseSegment> segments) {
322             Objects.requireNonNull(segments);
323             mSegments.clear();
324             mSegments.addAll(segments);
325             return this;
326         }
327 
328         /**
329          * Sets if the session contains route. Set by platform to indicate whether the route can be
330          * requested via UI intent.
331          *
332          * @param hasRoute flag whether the session has recorded {@link ExerciseRoute}
333          * @hide
334          */
335         @NonNull
setHasRoute(boolean hasRoute)336         public Builder setHasRoute(boolean hasRoute) {
337             mHasRoute = hasRoute;
338             return this;
339         }
340 
341         /**
342          * @return Object of {@link ExerciseSessionRecord} without validating the values.
343          * @hide
344          */
345         @NonNull
buildWithoutValidation()346         public ExerciseSessionRecord buildWithoutValidation() {
347             return new ExerciseSessionRecord(
348                     mMetadata,
349                     mStartTime,
350                     mStartZoneOffset,
351                     mEndTime,
352                     mEndZoneOffset,
353                     mNotes,
354                     mExerciseType,
355                     mTitle,
356                     mRoute,
357                     mHasRoute,
358                     mSegments,
359                     mLaps,
360                     true);
361         }
362 
363         /** Returns {@link ExerciseSessionRecord} */
364         @NonNull
build()365         public ExerciseSessionRecord build() {
366             return new ExerciseSessionRecord(
367                     mMetadata,
368                     mStartTime,
369                     mStartZoneOffset,
370                     mEndTime,
371                     mEndZoneOffset,
372                     mNotes,
373                     mExerciseType,
374                     mTitle,
375                     mRoute,
376                     mHasRoute,
377                     mSegments,
378                     mLaps,
379                     false);
380         }
381     }
382 
383     /** @hide */
384     @Override
toRecordInternal()385     public ExerciseSessionRecordInternal toRecordInternal() {
386         ExerciseSessionRecordInternal recordInternal =
387                 (ExerciseSessionRecordInternal)
388                         new ExerciseSessionRecordInternal()
389                                 .setUuid(getMetadata().getId())
390                                 .setPackageName(getMetadata().getDataOrigin().getPackageName())
391                                 .setLastModifiedTime(
392                                         getMetadata().getLastModifiedTime().toEpochMilli())
393                                 .setClientRecordId(getMetadata().getClientRecordId())
394                                 .setClientRecordVersion(getMetadata().getClientRecordVersion())
395                                 .setManufacturer(getMetadata().getDevice().getManufacturer())
396                                 .setModel(getMetadata().getDevice().getModel())
397                                 .setDeviceType(getMetadata().getDevice().getType())
398                                 .setRecordingMethod(getMetadata().getRecordingMethod());
399         recordInternal.setStartTime(getStartTime().toEpochMilli());
400         recordInternal.setEndTime(getEndTime().toEpochMilli());
401         recordInternal.setStartZoneOffset(getStartZoneOffset().getTotalSeconds());
402         recordInternal.setEndZoneOffset(getEndZoneOffset().getTotalSeconds());
403 
404         if (getNotes() != null) {
405             recordInternal.setNotes(getNotes().toString());
406         }
407 
408         if (getTitle() != null) {
409             recordInternal.setTitle(getTitle().toString());
410         }
411 
412         if (getRoute() != null) {
413             recordInternal.setRoute(getRoute().toRouteInternal());
414         }
415 
416         if (getLaps() != null && !getLaps().isEmpty()) {
417             recordInternal.setExerciseLaps(
418                     getLaps().stream()
419                             .map(ExerciseLap::toExerciseLapInternal)
420                             .collect(Collectors.toList()));
421         }
422 
423         if (getSegments() != null && !getSegments().isEmpty()) {
424             recordInternal.setExerciseSegments(
425                     getSegments().stream()
426                             .map(ExerciseSegment::toSegmentInternal)
427                             .collect(Collectors.toList()));
428         }
429         recordInternal.setExerciseType(mExerciseType);
430         return recordInternal;
431     }
432 }
433