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.healthconnect.cts.aggregation;
18 
19 import static android.health.connect.datatypes.ExerciseSessionRecord.EXERCISE_DURATION_TOTAL;
20 import static android.healthconnect.cts.utils.DataFactory.SESSION_END_TIME;
21 import static android.healthconnect.cts.utils.DataFactory.SESSION_START_TIME;
22 import static android.healthconnect.cts.utils.DataFactory.generateMetadata;
23 import static android.healthconnect.cts.utils.TestUtils.getAggregateResponse;
24 import static android.healthconnect.cts.utils.TestUtils.insertRecord;
25 import static android.healthconnect.cts.utils.TestUtils.setupAggregation;
26 
27 import static com.google.common.truth.Truth.assertThat;
28 
29 import android.health.connect.AggregateRecordsGroupedByDurationResponse;
30 import android.health.connect.AggregateRecordsRequest;
31 import android.health.connect.AggregateRecordsResponse;
32 import android.health.connect.HealthDataCategory;
33 import android.health.connect.LocalTimeRangeFilter;
34 import android.health.connect.TimeInstantRangeFilter;
35 import android.health.connect.datatypes.ExerciseSegment;
36 import android.health.connect.datatypes.ExerciseSegmentType;
37 import android.health.connect.datatypes.ExerciseSessionRecord;
38 import android.health.connect.datatypes.ExerciseSessionType;
39 import android.healthconnect.cts.utils.AssumptionCheckerRule;
40 import android.healthconnect.cts.utils.TestUtils;
41 
42 import org.junit.After;
43 import org.junit.Before;
44 import org.junit.Rule;
45 import org.junit.Test;
46 
47 import java.time.Duration;
48 import java.time.Instant;
49 import java.time.LocalDateTime;
50 import java.time.ZoneOffset;
51 import java.time.temporal.ChronoUnit;
52 import java.util.List;
53 
54 public class ExerciseDurationAggregationTest {
55     private final TimeInstantRangeFilter mFilterAllSession =
56             new TimeInstantRangeFilter.Builder()
57                     .setStartTime(Instant.EPOCH)
58                     .setEndTime(Instant.now().plusSeconds(1000))
59                     .build();
60 
61     private final TimeInstantRangeFilter mFilterSmallWindow =
62             new TimeInstantRangeFilter.Builder()
63                     .setStartTime(SESSION_START_TIME)
64                     .setEndTime(SESSION_END_TIME)
65                     .build();
66 
67     private final AggregateRecordsRequest<Long> mAggregateAllRecordsRequest =
68             new AggregateRecordsRequest.Builder<Long>(mFilterAllSession)
69                     .addAggregationType(EXERCISE_DURATION_TOTAL)
70                     .build();
71 
72     private final AggregateRecordsRequest<Long> mAggregateInSmallWindow =
73             new AggregateRecordsRequest.Builder<Long>(mFilterSmallWindow)
74                     .addAggregationType(EXERCISE_DURATION_TOTAL)
75                     .build();
76 
77     private static final String PACKAGE_NAME = "android.healthconnect.cts";
78 
79     @Rule
80     public AssumptionCheckerRule mSupportedHardwareRule =
81             new AssumptionCheckerRule(
82                     TestUtils::isHardwareSupported, "Tests should run on supported hardware only.");
83 
84     @Before
setUp()85     public void setUp() throws InterruptedException {
86         TestUtils.deleteAllStagedRemoteData();
87     }
88 
89     @After
tearDown()90     public void tearDown() throws InterruptedException {
91         TestUtils.verifyDeleteRecords(
92                 ExerciseSessionRecord.class,
93                 new TimeInstantRangeFilter.Builder()
94                         .setStartTime(Instant.EPOCH)
95                         .setEndTime(Instant.now())
96                         .build());
97     }
98 
99     @Test
testSimpleAggregation_oneSession_returnsItsDuration()100     public void testSimpleAggregation_oneSession_returnsItsDuration() throws InterruptedException {
101         setupAggregation(PACKAGE_NAME, HealthDataCategory.ACTIVITY);
102         ExerciseSessionRecord session =
103                 new ExerciseSessionRecord.Builder(
104                                 generateMetadata(),
105                                 SESSION_START_TIME,
106                                 SESSION_END_TIME,
107                                 ExerciseSessionType
108                                         .EXERCISE_SESSION_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING)
109                         .build();
110         insertRecord(session);
111         AggregateRecordsResponse<Long> response = getAggregateResponse(mAggregateAllRecordsRequest);
112 
113         assertThat(response.get(EXERCISE_DURATION_TOTAL)).isNotNull();
114         assertThat(response.get(EXERCISE_DURATION_TOTAL))
115                 .isEqualTo(
116                         session.getEndTime().toEpochMilli()
117                                 - session.getStartTime().toEpochMilli());
118         assertThat(response.getZoneOffset(EXERCISE_DURATION_TOTAL))
119                 .isEqualTo(session.getStartZoneOffset());
120     }
121 
122     @Test
testSimpleAggregation_oneSessionStartEarlierThanWindow_returnsOverlapDuration()123     public void testSimpleAggregation_oneSessionStartEarlierThanWindow_returnsOverlapDuration()
124             throws InterruptedException {
125         setupAggregation(PACKAGE_NAME, HealthDataCategory.ACTIVITY);
126         ExerciseSessionRecord session =
127                 new ExerciseSessionRecord.Builder(
128                                 generateMetadata(),
129                                 SESSION_START_TIME.minusSeconds(10),
130                                 SESSION_END_TIME,
131                                 ExerciseSessionType
132                                         .EXERCISE_SESSION_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING)
133                         .build();
134         insertRecord(session);
135         AggregateRecordsResponse<Long> response = getAggregateResponse(mAggregateInSmallWindow);
136 
137         assertThat(response.get(EXERCISE_DURATION_TOTAL)).isNotNull();
138         assertThat(response.get(EXERCISE_DURATION_TOTAL))
139                 .isEqualTo(SESSION_END_TIME.toEpochMilli() - SESSION_START_TIME.toEpochMilli());
140         assertThat(response.getZoneOffset(EXERCISE_DURATION_TOTAL))
141                 .isEqualTo(session.getStartZoneOffset());
142     }
143 
144     @Test
testSimpleAggregation_oneSessionBiggerThanWindow_returnsOverlapDuration()145     public void testSimpleAggregation_oneSessionBiggerThanWindow_returnsOverlapDuration()
146             throws InterruptedException {
147         setupAggregation(PACKAGE_NAME, HealthDataCategory.ACTIVITY);
148 
149         ExerciseSessionRecord session =
150                 new ExerciseSessionRecord.Builder(
151                                 generateMetadata(),
152                                 SESSION_START_TIME.minusSeconds(100),
153                                 SESSION_END_TIME.plusSeconds(100),
154                                 ExerciseSessionType
155                                         .EXERCISE_SESSION_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING)
156                         .build();
157         insertRecord(session);
158         AggregateRecordsResponse<Long> response = getAggregateResponse(mAggregateInSmallWindow);
159 
160         assertThat(response.get(EXERCISE_DURATION_TOTAL)).isNotNull();
161         assertThat(response.get(EXERCISE_DURATION_TOTAL))
162                 .isEqualTo(SESSION_END_TIME.toEpochMilli() - SESSION_START_TIME.toEpochMilli());
163         assertThat(response.getZoneOffset(EXERCISE_DURATION_TOTAL))
164                 .isEqualTo(session.getStartZoneOffset());
165     }
166 
167     @Test
testSimpleAggregation_oneSessionWithRest_returnsDurationMinusRest()168     public void testSimpleAggregation_oneSessionWithRest_returnsDurationMinusRest()
169             throws InterruptedException {
170         setupAggregation(PACKAGE_NAME, HealthDataCategory.ACTIVITY);
171 
172         ExerciseSegment restSegment =
173                 new ExerciseSegment.Builder(
174                                 SESSION_START_TIME,
175                                 SESSION_START_TIME.plusSeconds(100),
176                                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_REST)
177                         .build();
178         ExerciseSessionRecord session =
179                 new ExerciseSessionRecord.Builder(
180                                 generateMetadata(),
181                                 SESSION_START_TIME,
182                                 SESSION_END_TIME,
183                                 ExerciseSessionType.EXERCISE_SESSION_TYPE_CALISTHENICS)
184                         .setSegments(
185                                 List.of(
186                                         restSegment,
187                                         new ExerciseSegment.Builder(
188                                                         SESSION_START_TIME.plusSeconds(200),
189                                                         SESSION_START_TIME.plusSeconds(600),
190                                                         ExerciseSegmentType
191                                                                 .EXERCISE_SEGMENT_TYPE_BURPEE)
192                                                 .build()))
193                         .build();
194 
195         insertRecord(session);
196         AggregateRecordsResponse<Long> response = getAggregateResponse(mAggregateAllRecordsRequest);
197 
198         assertThat(response.get(EXERCISE_DURATION_TOTAL)).isNotNull();
199 
200         long restDuration =
201                 restSegment.getEndTime().toEpochMilli() - restSegment.getStartTime().toEpochMilli();
202         assertThat(response.get(EXERCISE_DURATION_TOTAL))
203                 .isEqualTo(
204                         session.getEndTime().toEpochMilli()
205                                 - session.getStartTime().toEpochMilli()
206                                 - restDuration);
207     }
208 
209     @Test
testAggregationByDuration_oneSession_returnsSplitDurationIntoGroups()210     public void testAggregationByDuration_oneSession_returnsSplitDurationIntoGroups()
211             throws InterruptedException {
212         setupAggregation(PACKAGE_NAME, HealthDataCategory.ACTIVITY);
213         Instant endTime = SESSION_START_TIME.plus(10, ChronoUnit.HOURS);
214         ExerciseSessionRecord session =
215                 new ExerciseSessionRecord.Builder(
216                                 generateMetadata(),
217                                 SESSION_START_TIME,
218                                 endTime,
219                                 ExerciseSessionType.EXERCISE_SESSION_TYPE_BADMINTON)
220                         .build();
221         insertRecord(session);
222 
223         List<AggregateRecordsGroupedByDurationResponse<Long>> responses =
224                 TestUtils.getAggregateResponseGroupByDuration(
225                         new AggregateRecordsRequest.Builder<Long>(
226                                         new TimeInstantRangeFilter.Builder()
227                                                 .setStartTime(SESSION_START_TIME)
228                                                 .setEndTime(endTime)
229                                                 .build())
230                                 .addAggregationType(EXERCISE_DURATION_TOTAL)
231                                 .build(),
232                         Duration.of(1, ChronoUnit.HOURS));
233 
234         assertThat(responses).isNotEmpty();
235         assertThat(responses.size()).isEqualTo(10);
236         for (AggregateRecordsGroupedByDurationResponse<Long> response : responses) {
237             assertThat(response.get(EXERCISE_DURATION_TOTAL)).isEqualTo(3600000);
238         }
239     }
240 
241     @Test
testAggregation_oneSessionLocalTimeFilter_findsSessionWithMinOffset()242     public void testAggregation_oneSessionLocalTimeFilter_findsSessionWithMinOffset()
243             throws InterruptedException {
244         setupAggregation(PACKAGE_NAME, HealthDataCategory.ACTIVITY);
245         Instant endTime = Instant.now();
246         LocalDateTime endTimeLocal = LocalDateTime.ofInstant(endTime, ZoneOffset.UTC);
247 
248         long sessionDurationSeconds = 3600;
249         ExerciseSessionRecord session =
250                 new ExerciseSessionRecord.Builder(
251                                 generateMetadata(),
252                                 endTime.minusSeconds(sessionDurationSeconds),
253                                 endTime,
254                                 ExerciseSessionType.EXERCISE_SESSION_TYPE_BADMINTON)
255                         .setStartZoneOffset(ZoneOffset.MIN)
256                         .setEndZoneOffset(ZoneOffset.MIN)
257                         .build();
258 
259         insertRecord(session);
260         AggregateRecordsResponse<Long> response =
261                 getAggregateResponse(
262                         new AggregateRecordsRequest.Builder<Long>(
263                                         new LocalTimeRangeFilter.Builder()
264                                                 .setStartTime(endTimeLocal.minusHours(25))
265                                                 .setEndTime(endTimeLocal.minusHours(15))
266                                                 .build())
267                                 .addAggregationType(EXERCISE_DURATION_TOTAL)
268                                 .build());
269 
270         assertThat(response.get(EXERCISE_DURATION_TOTAL)).isEqualTo(sessionDurationSeconds * 1000);
271     }
272 
273     @Test
testAggregation_oneSessionLocalTimeFilterExcludeSegment_substractsExcludeInterval()274     public void testAggregation_oneSessionLocalTimeFilterExcludeSegment_substractsExcludeInterval()
275             throws InterruptedException {
276         setupAggregation(PACKAGE_NAME, HealthDataCategory.ACTIVITY);
277         Instant endTime = SESSION_START_TIME.plus(1, ChronoUnit.HOURS);
278         ExerciseSessionRecord session =
279                 new ExerciseSessionRecord.Builder(
280                                 generateMetadata(),
281                                 SESSION_START_TIME,
282                                 endTime,
283                                 ExerciseSessionType.EXERCISE_SESSION_TYPE_BADMINTON)
284                         .setStartZoneOffset(ZoneOffset.MIN)
285                         .setEndZoneOffset(ZoneOffset.MIN)
286                         .setSegments(
287                                 List.of(
288                                         new ExerciseSegment.Builder(
289                                                         SESSION_START_TIME.plusSeconds(10),
290                                                         endTime.minusSeconds(10),
291                                                         ExerciseSegmentType
292                                                                 .EXERCISE_SEGMENT_TYPE_PAUSE)
293                                                 .build()))
294                         .build();
295 
296         LocalDateTime endTimeLocal = LocalDateTime.ofInstant(endTime, ZoneOffset.UTC);
297         insertRecord(session);
298         AggregateRecordsResponse<Long> response =
299                 getAggregateResponse(
300                         new AggregateRecordsRequest.Builder<Long>(
301                                         new LocalTimeRangeFilter.Builder()
302                                                 .setStartTime(endTimeLocal.minusHours(25))
303                                                 .setEndTime(endTimeLocal.minusHours(15))
304                                                 .build())
305                                 .addAggregationType(EXERCISE_DURATION_TOTAL)
306                                 .build());
307 
308         assertThat(response.get(EXERCISE_DURATION_TOTAL)).isEqualTo(20000);
309     }
310 }
311