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