1 /*
2  * Copyright (C) 2022 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.datatypes;
18 
19 import static android.health.connect.datatypes.SpeedRecord.SPEED_AVG;
20 import static android.health.connect.datatypes.SpeedRecord.SPEED_MAX;
21 import static android.health.connect.datatypes.SpeedRecord.SPEED_MIN;
22 import static android.healthconnect.cts.utils.TestUtils.copyRecordIdsViaReflection;
23 import static android.healthconnect.cts.utils.TestUtils.distinctByUuid;
24 import static android.healthconnect.cts.utils.TestUtils.getRecordIds;
25 import static android.healthconnect.cts.utils.TestUtils.insertRecords;
26 import static android.healthconnect.cts.utils.TestUtils.readRecords;
27 import static android.healthconnect.cts.utils.TestUtils.updateRecords;
28 
29 import static com.google.common.truth.Truth.assertThat;
30 
31 import android.content.Context;
32 import android.health.connect.AggregateRecordsRequest;
33 import android.health.connect.AggregateRecordsResponse;
34 import android.health.connect.DeleteUsingFiltersRequest;
35 import android.health.connect.HealthConnectException;
36 import android.health.connect.HealthDataCategory;
37 import android.health.connect.ReadRecordsRequestUsingFilters;
38 import android.health.connect.ReadRecordsRequestUsingIds;
39 import android.health.connect.RecordIdFilter;
40 import android.health.connect.TimeInstantRangeFilter;
41 import android.health.connect.changelog.ChangeLogTokenRequest;
42 import android.health.connect.changelog.ChangeLogTokenResponse;
43 import android.health.connect.changelog.ChangeLogsRequest;
44 import android.health.connect.changelog.ChangeLogsResponse;
45 import android.health.connect.datatypes.AggregationType;
46 import android.health.connect.datatypes.DataOrigin;
47 import android.health.connect.datatypes.Device;
48 import android.health.connect.datatypes.Metadata;
49 import android.health.connect.datatypes.Record;
50 import android.health.connect.datatypes.SpeedRecord;
51 import android.health.connect.datatypes.units.Velocity;
52 import android.healthconnect.cts.utils.AssumptionCheckerRule;
53 import android.healthconnect.cts.utils.TestUtils;
54 import android.platform.test.annotations.AppModeFull;
55 
56 import androidx.test.core.app.ApplicationProvider;
57 import androidx.test.runner.AndroidJUnit4;
58 
59 import org.junit.After;
60 import org.junit.Assert;
61 import org.junit.Before;
62 import org.junit.Rule;
63 import org.junit.Test;
64 import org.junit.runner.RunWith;
65 
66 import java.time.Instant;
67 import java.time.ZoneOffset;
68 import java.time.temporal.ChronoUnit;
69 import java.util.ArrayList;
70 import java.util.Arrays;
71 import java.util.Collections;
72 import java.util.List;
73 import java.util.Set;
74 import java.util.UUID;
75 
76 @AppModeFull(reason = "HealthConnectManager is not accessible to instant apps")
77 @RunWith(AndroidJUnit4.class)
78 public class SpeedRecordTest {
79 
80     private static final String TAG = "SpeedRecordTest";
81     private static final String PACKAGE_NAME = "android.healthconnect.cts";
82 
83     @Rule
84     public AssumptionCheckerRule mSupportedHardwareRule =
85             new AssumptionCheckerRule(
86                     TestUtils::isHardwareSupported, "Tests should run on supported hardware only.");
87 
88     @Before
setUp()89     public void setUp() throws InterruptedException {
90         TestUtils.deleteAllStagedRemoteData();
91     }
92 
93     @After
tearDown()94     public void tearDown() throws InterruptedException {
95         TestUtils.verifyDeleteRecords(
96                 SpeedRecord.class,
97                 new TimeInstantRangeFilter.Builder()
98                         .setStartTime(Instant.EPOCH)
99                         .setEndTime(Instant.now())
100                         .build());
101         TestUtils.deleteAllStagedRemoteData();
102     }
103 
104     @Test
testInsertSpeedRecord()105     public void testInsertSpeedRecord() throws InterruptedException {
106         TestUtils.insertRecords(Arrays.asList(getBaseSpeedRecord(), getCompleteSpeedRecord()));
107     }
108 
109     @Test
testReadSpeedRecord_usingIds()110     public void testReadSpeedRecord_usingIds() throws InterruptedException {
111         List<Record> recordList =
112                 insertRecords(Arrays.asList(getCompleteSpeedRecord(), getCompleteSpeedRecord()));
113 
114         readSpeedRecordUsingIds(recordList);
115     }
116 
117     @Test
testReadSpeedRecord_invalidIds()118     public void testReadSpeedRecord_invalidIds() throws InterruptedException {
119         ReadRecordsRequestUsingIds<SpeedRecord> request =
120                 new ReadRecordsRequestUsingIds.Builder<>(SpeedRecord.class)
121                         .addId(UUID.randomUUID().toString())
122                         .build();
123         List<SpeedRecord> result = TestUtils.readRecords(request);
124         assertThat(result.size()).isEqualTo(0);
125     }
126 
127     @Test
testReadSpeedRecord_usingClientRecordIds()128     public void testReadSpeedRecord_usingClientRecordIds() throws InterruptedException {
129         List<Record> recordList = Arrays.asList(getCompleteSpeedRecord(), getCompleteSpeedRecord());
130         List<Record> insertedRecords = TestUtils.insertRecords(recordList);
131         readSpeedRecordUsingClientId(insertedRecords);
132     }
133 
134     @Test
testReadSpeedRecord_invalidClientRecordIds()135     public void testReadSpeedRecord_invalidClientRecordIds() throws InterruptedException {
136         ReadRecordsRequestUsingIds<SpeedRecord> request =
137                 new ReadRecordsRequestUsingIds.Builder<>(SpeedRecord.class)
138                         .addClientRecordId("abc")
139                         .build();
140         List<SpeedRecord> result = TestUtils.readRecords(request);
141         assertThat(result.size()).isEqualTo(0);
142     }
143 
144     @Test
testReadSpeedRecordUsingFilters_default()145     public void testReadSpeedRecordUsingFilters_default() throws InterruptedException {
146         List<SpeedRecord> oldSpeedRecords =
147                 TestUtils.readRecords(
148                         new ReadRecordsRequestUsingFilters.Builder<>(SpeedRecord.class).build());
149 
150         SpeedRecord testRecord = (SpeedRecord) TestUtils.insertRecord(getCompleteSpeedRecord());
151         List<SpeedRecord> newSpeedRecords =
152                 TestUtils.readRecords(
153                         new ReadRecordsRequestUsingFilters.Builder<>(SpeedRecord.class).build());
154         assertThat(newSpeedRecords.size()).isEqualTo(oldSpeedRecords.size() + 1);
155         assertThat(newSpeedRecords.get(newSpeedRecords.size() - 1).equals(testRecord)).isTrue();
156     }
157 
158     @Test
testReadSpeedRecordUsingFilters_timeFilter()159     public void testReadSpeedRecordUsingFilters_timeFilter() throws InterruptedException {
160         TimeInstantRangeFilter filter =
161                 new TimeInstantRangeFilter.Builder()
162                         .setStartTime(Instant.now())
163                         .setEndTime(Instant.now().plusMillis(3000))
164                         .build();
165 
166         SpeedRecord testRecord = (SpeedRecord) TestUtils.insertRecord(getCompleteSpeedRecord());
167         List<SpeedRecord> newSpeedRecords =
168                 TestUtils.readRecords(
169                         new ReadRecordsRequestUsingFilters.Builder<>(SpeedRecord.class)
170                                 .setTimeRangeFilter(filter)
171                                 .build());
172         assertThat(newSpeedRecords.size()).isEqualTo(1);
173         assertThat(newSpeedRecords.get(newSpeedRecords.size() - 1).equals(testRecord)).isTrue();
174     }
175 
176     @Test
testReadSpeedRecordUsingFilters_dataFilter_correct()177     public void testReadSpeedRecordUsingFilters_dataFilter_correct() throws InterruptedException {
178         Context context = ApplicationProvider.getApplicationContext();
179         List<SpeedRecord> oldSpeedRecords =
180                 TestUtils.readRecords(
181                         new ReadRecordsRequestUsingFilters.Builder<>(SpeedRecord.class)
182                                 .addDataOrigins(
183                                         new DataOrigin.Builder()
184                                                 .setPackageName(context.getPackageName())
185                                                 .build())
186                                 .build());
187 
188         SpeedRecord testRecord = (SpeedRecord) TestUtils.insertRecord(getCompleteSpeedRecord());
189         List<SpeedRecord> newSpeedRecords =
190                 TestUtils.readRecords(
191                         new ReadRecordsRequestUsingFilters.Builder<>(SpeedRecord.class)
192                                 .addDataOrigins(
193                                         new DataOrigin.Builder()
194                                                 .setPackageName(context.getPackageName())
195                                                 .build())
196                                 .build());
197         assertThat(newSpeedRecords.size() - oldSpeedRecords.size()).isEqualTo(1);
198         assertThat(newSpeedRecords.get(newSpeedRecords.size() - 1).equals(testRecord)).isTrue();
199         SpeedRecord newRecord = newSpeedRecords.get(newSpeedRecords.size() - 1);
200         assertThat(newRecord.equals(testRecord)).isTrue();
201         for (int idx = 0; idx < newRecord.getSamples().size(); idx++) {
202             assertThat(newRecord.getSamples().get(idx).getTime().toEpochMilli())
203                     .isEqualTo(testRecord.getSamples().get(idx).getTime().toEpochMilli());
204             assertThat(newRecord.getSamples().get(idx).getSpeed())
205                     .isEqualTo(testRecord.getSamples().get(idx).getSpeed());
206         }
207     }
208 
209     @Test
testReadSpeedRecordUsingFilters_dataFilter_incorrect()210     public void testReadSpeedRecordUsingFilters_dataFilter_incorrect() throws InterruptedException {
211         TestUtils.insertRecords(Collections.singletonList(getCompleteSpeedRecord()));
212         List<SpeedRecord> newSpeedRecords =
213                 TestUtils.readRecords(
214                         new ReadRecordsRequestUsingFilters.Builder<>(SpeedRecord.class)
215                                 .addDataOrigins(
216                                         new DataOrigin.Builder().setPackageName("abc").build())
217                                 .build());
218         assertThat(newSpeedRecords.size()).isEqualTo(0);
219     }
220 
221     @Test
testDeleteSpeedRecord_no_filters()222     public void testDeleteSpeedRecord_no_filters() throws InterruptedException {
223         String id = TestUtils.insertRecordAndGetId(getCompleteSpeedRecord());
224         TestUtils.verifyDeleteRecords(new DeleteUsingFiltersRequest.Builder().build());
225         TestUtils.assertRecordNotFound(id, SpeedRecord.class);
226     }
227 
228     @Test
testDeleteSpeedRecord_time_filters()229     public void testDeleteSpeedRecord_time_filters() throws InterruptedException {
230         TimeInstantRangeFilter timeRangeFilter =
231                 new TimeInstantRangeFilter.Builder()
232                         .setStartTime(Instant.now())
233                         .setEndTime(Instant.now().plusMillis(1000))
234                         .build();
235         String id = TestUtils.insertRecordAndGetId(getCompleteSpeedRecord());
236         TestUtils.verifyDeleteRecords(
237                 new DeleteUsingFiltersRequest.Builder()
238                         .addRecordType(SpeedRecord.class)
239                         .setTimeRangeFilter(timeRangeFilter)
240                         .build());
241         TestUtils.assertRecordNotFound(id, SpeedRecord.class);
242     }
243 
244     @Test
testDeleteSpeedRecord_recordId_filters()245     public void testDeleteSpeedRecord_recordId_filters() throws InterruptedException {
246         List<Record> records =
247                 TestUtils.insertRecords(List.of(getBaseSpeedRecord(), getCompleteSpeedRecord()));
248 
249         for (Record record : records) {
250             TestUtils.verifyDeleteRecords(
251                     new DeleteUsingFiltersRequest.Builder()
252                             .addRecordType(record.getClass())
253                             .build());
254             TestUtils.assertRecordNotFound(record.getMetadata().getId(), record.getClass());
255         }
256     }
257 
258     @Test
testDeleteSpeedRecord_dataOrigin_filters()259     public void testDeleteSpeedRecord_dataOrigin_filters() throws InterruptedException {
260         Context context = ApplicationProvider.getApplicationContext();
261         String id = TestUtils.insertRecordAndGetId(getCompleteSpeedRecord());
262         TestUtils.verifyDeleteRecords(
263                 new DeleteUsingFiltersRequest.Builder()
264                         .addDataOrigin(
265                                 new DataOrigin.Builder()
266                                         .setPackageName(context.getPackageName())
267                                         .build())
268                         .build());
269         TestUtils.assertRecordNotFound(id, SpeedRecord.class);
270     }
271 
272     @Test
testDeleteSpeedRecord_dataOrigin_filter_incorrect()273     public void testDeleteSpeedRecord_dataOrigin_filter_incorrect() throws InterruptedException {
274         String id = TestUtils.insertRecordAndGetId(getCompleteSpeedRecord());
275         TestUtils.verifyDeleteRecords(
276                 new DeleteUsingFiltersRequest.Builder()
277                         .addDataOrigin(new DataOrigin.Builder().setPackageName("abc").build())
278                         .build());
279         TestUtils.assertRecordFound(id, SpeedRecord.class);
280     }
281 
282     @Test
testDeleteSpeedRecord_usingIds()283     public void testDeleteSpeedRecord_usingIds() throws InterruptedException {
284         List<Record> records =
285                 TestUtils.insertRecords(List.of(getBaseSpeedRecord(), getCompleteSpeedRecord()));
286         List<RecordIdFilter> recordIds = new ArrayList<>(records.size());
287         for (Record record : records) {
288             recordIds.add(RecordIdFilter.fromId(record.getClass(), record.getMetadata().getId()));
289         }
290 
291         TestUtils.verifyDeleteRecords(recordIds);
292         for (Record record : records) {
293             TestUtils.assertRecordNotFound(record.getMetadata().getId(), record.getClass());
294         }
295     }
296 
297     @Test
testDeleteSpeedRecord_time_range()298     public void testDeleteSpeedRecord_time_range() throws InterruptedException {
299         TimeInstantRangeFilter timeRangeFilter =
300                 new TimeInstantRangeFilter.Builder()
301                         .setStartTime(Instant.now())
302                         .setEndTime(Instant.now().plusMillis(1000))
303                         .build();
304         String id = TestUtils.insertRecordAndGetId(getCompleteSpeedRecord());
305         TestUtils.verifyDeleteRecords(SpeedRecord.class, timeRangeFilter);
306         TestUtils.assertRecordNotFound(id, SpeedRecord.class);
307     }
308 
309     @Test
testZoneOffsets()310     public void testZoneOffsets() {
311         final ZoneOffset defaultZoneOffset =
312                 ZoneOffset.systemDefault().getRules().getOffset(Instant.now());
313         final ZoneOffset startZoneOffset = ZoneOffset.UTC;
314         final ZoneOffset endZoneOffset = ZoneOffset.MAX;
315         SpeedRecord.Builder builder =
316                 new SpeedRecord.Builder(
317                         new Metadata.Builder().build(),
318                         Instant.now(),
319                         Instant.now().plusMillis(1000),
320                         Collections.emptyList());
321 
322         assertThat(builder.setStartZoneOffset(startZoneOffset).build().getStartZoneOffset())
323                 .isEqualTo(startZoneOffset);
324         assertThat(builder.setEndZoneOffset(endZoneOffset).build().getEndZoneOffset())
325                 .isEqualTo(endZoneOffset);
326         assertThat(builder.clearStartZoneOffset().build().getStartZoneOffset())
327                 .isEqualTo(defaultZoneOffset);
328         assertThat(builder.clearEndZoneOffset().build().getEndZoneOffset())
329                 .isEqualTo(defaultZoneOffset);
330     }
331 
332     @Test
testInsertAndDeleteRecord_changelogs()333     public void testInsertAndDeleteRecord_changelogs() throws InterruptedException {
334         Context context = ApplicationProvider.getApplicationContext();
335         ChangeLogTokenResponse tokenResponse =
336                 TestUtils.getChangeLogToken(
337                         new ChangeLogTokenRequest.Builder()
338                                 .addDataOriginFilter(
339                                         new DataOrigin.Builder()
340                                                 .setPackageName(context.getPackageName())
341                                                 .build())
342                                 .addRecordType(SpeedRecord.class)
343                                 .build());
344         ChangeLogsRequest changeLogsRequest =
345                 new ChangeLogsRequest.Builder(tokenResponse.getToken()).build();
346         ChangeLogsResponse response = TestUtils.getChangeLogs(changeLogsRequest);
347         assertThat(response.getUpsertedRecords().size()).isEqualTo(0);
348         assertThat(response.getDeletedLogs().size()).isEqualTo(0);
349 
350         List<Record> testRecord =
351                 TestUtils.insertRecords(Collections.singletonList(getCompleteSpeedRecord()));
352         response = TestUtils.getChangeLogs(changeLogsRequest);
353         assertThat(response.getUpsertedRecords().size()).isEqualTo(1);
354         assertThat(
355                         response.getUpsertedRecords().stream()
356                                 .map(Record::getMetadata)
357                                 .map(Metadata::getId)
358                                 .toList())
359                 .containsExactlyElementsIn(
360                         testRecord.stream().map(Record::getMetadata).map(Metadata::getId).toList());
361         assertThat(response.getDeletedLogs().size()).isEqualTo(0);
362 
363         TestUtils.verifyDeleteRecords(
364                 new DeleteUsingFiltersRequest.Builder().addRecordType(SpeedRecord.class).build());
365         response = TestUtils.getChangeLogs(changeLogsRequest);
366         assertThat(response.getDeletedLogs()).hasSize(testRecord.size());
367         assertThat(
368                         response.getDeletedLogs().stream()
369                                 .map(ChangeLogsResponse.DeletedLog::getDeletedRecordId)
370                                 .toList())
371                 .containsExactlyElementsIn(
372                         testRecord.stream().map(Record::getMetadata).map(Metadata::getId).toList());
373     }
374 
375     @Test
testSpeedAggregation_getAggregationFromThreerecords_aggResponsesAreCorrect()376     public void testSpeedAggregation_getAggregationFromThreerecords_aggResponsesAreCorrect()
377             throws Exception {
378         TestUtils.setupAggregation(PACKAGE_NAME, HealthDataCategory.ACTIVITY);
379         List<Record> records =
380                 Arrays.asList(
381                         buildRecordForSpeed(120, 100),
382                         buildRecordForSpeed(80, 101),
383                         buildRecordForSpeed(100, 102));
384         AggregateRecordsResponse<Velocity> response =
385                 TestUtils.getAggregateResponse(
386                         new AggregateRecordsRequest.Builder<Velocity>(
387                                         new TimeInstantRangeFilter.Builder()
388                                                 .setStartTime(Instant.ofEpochMilli(0))
389                                                 .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
390                                                 .build())
391                                 .addAggregationType(SPEED_MAX)
392                                 .addAggregationType(SPEED_AVG)
393                                 .addAggregationType(SPEED_MIN)
394                                 .build(),
395                         records);
396         checkAggregationResult(SPEED_MIN, Velocity.fromMetersPerSecond(80), response);
397         checkAggregationResult(SPEED_AVG, Velocity.fromMetersPerSecond(100), response);
398         checkAggregationResult(SPEED_MAX, Velocity.fromMetersPerSecond(120), response);
399     }
400 
checkAggregationResult( AggregationType<Velocity> type, Velocity expectedResult, AggregateRecordsResponse<Velocity> response)401     private void checkAggregationResult(
402             AggregationType<Velocity> type,
403             Velocity expectedResult,
404             AggregateRecordsResponse<Velocity> response) {
405         assertThat(response.get(type)).isNotNull();
406         assertThat(response.get(type)).isEqualTo(expectedResult);
407         assertThat(response.getZoneOffset(type))
408                 .isEqualTo(ZoneOffset.systemDefault().getRules().getOffset(Instant.now()));
409         Set<DataOrigin> dataOrigins = response.getDataOrigins(type);
410         assertThat(dataOrigins).hasSize(1);
411         for (DataOrigin itr : dataOrigins) {
412             assertThat(itr.getPackageName()).isEqualTo("android.healthconnect.cts");
413         }
414     }
415 
readSpeedRecordUsingClientId(List<? extends Record> insertedRecord)416     private void readSpeedRecordUsingClientId(List<? extends Record> insertedRecord)
417             throws InterruptedException {
418         ReadRecordsRequestUsingIds.Builder<SpeedRecord> request =
419                 new ReadRecordsRequestUsingIds.Builder<>(SpeedRecord.class);
420         for (Record record : insertedRecord) {
421             request.addClientRecordId(record.getMetadata().getClientRecordId());
422         }
423         List<SpeedRecord> result = TestUtils.readRecords(request.build());
424         assertThat(result.size()).isEqualTo(insertedRecord.size());
425         assertThat(result).containsExactlyElementsIn(insertedRecord);
426     }
427 
readSpeedRecordUsingIds(List<? extends Record> recordList)428     private void readSpeedRecordUsingIds(List<? extends Record> recordList)
429             throws InterruptedException {
430         ReadRecordsRequestUsingIds.Builder<SpeedRecord> request =
431                 new ReadRecordsRequestUsingIds.Builder<>(SpeedRecord.class);
432         for (Record record : recordList) {
433             request.addId(record.getMetadata().getId());
434         }
435         ReadRecordsRequestUsingIds requestUsingIds = request.build();
436         assertThat(requestUsingIds.getRecordType()).isEqualTo(SpeedRecord.class);
437         assertThat(requestUsingIds.getRecordIdFilters()).isNotNull();
438         List<SpeedRecord> result = TestUtils.readRecords(requestUsingIds);
439         assertThat(result).hasSize(recordList.size());
440         assertThat(result.containsAll(recordList)).isTrue();
441     }
442 
443     @Test(expected = IllegalArgumentException.class)
testCreateSpeedRecord_invalidValue()444     public void testCreateSpeedRecord_invalidValue() {
445         new SpeedRecord.SpeedRecordSample(
446                 Velocity.fromMetersPerSecond(1000001), Instant.now().plusMillis(100));
447     }
448 
449     @Test(expected = IllegalArgumentException.class)
testCreateSpeedRecord_invalidSampleTime()450     public void testCreateSpeedRecord_invalidSampleTime() {
451         Instant startTime = Instant.now();
452         Instant endTime = startTime.plusMillis(100);
453         SpeedRecord.SpeedRecordSample speedRecord =
454                 new SpeedRecord.SpeedRecordSample(
455                         Velocity.fromMetersPerSecond(10.0), endTime.plusMillis(1));
456         ArrayList<SpeedRecord.SpeedRecordSample> speedRecords = new ArrayList<>();
457         speedRecords.add(speedRecord);
458         new SpeedRecord.Builder(new Metadata.Builder().build(), startTime, endTime, speedRecords)
459                 .build();
460     }
461 
462     @Test
testUpdateRecords_validInput_dataBaseUpdatedSuccessfully()463     public void testUpdateRecords_validInput_dataBaseUpdatedSuccessfully()
464             throws InterruptedException {
465 
466         List<Record> insertedRecords =
467                 TestUtils.insertRecords(
468                         Arrays.asList(getCompleteSpeedRecord(), getCompleteSpeedRecord()));
469 
470         // read inserted records and verify that the data is same as inserted.
471         readSpeedRecordUsingIds(insertedRecords);
472 
473         // Generate a new set of records that will be used to perform the update operation.
474         List<Record> updateRecords =
475                 Arrays.asList(getCompleteSpeedRecord(), getCompleteSpeedRecord());
476 
477         // Modify the uid of the updateRecords to the uuid that was present in the insert records.
478         for (int itr = 0; itr < updateRecords.size(); itr++) {
479             updateRecords.set(
480                     itr,
481                     getSpeedRecord_update(
482                             updateRecords.get(itr),
483                             insertedRecords.get(itr).getMetadata().getId(),
484                             insertedRecords.get(itr).getMetadata().getClientRecordId()));
485         }
486 
487         TestUtils.updateRecords(updateRecords);
488 
489         // assert the inserted data has been modified by reading the data.
490         readSpeedRecordUsingIds(updateRecords);
491     }
492 
493     @Test
testUpdateRecords_invalidInputRecords_noChangeInDataBase()494     public void testUpdateRecords_invalidInputRecords_noChangeInDataBase()
495             throws InterruptedException {
496         List<Record> insertedRecords =
497                 TestUtils.insertRecords(
498                         Arrays.asList(getCompleteSpeedRecord(), getCompleteSpeedRecord()));
499 
500         // read inserted records and verify that the data is same as inserted.
501         readSpeedRecordUsingIds(insertedRecords);
502 
503         // Generate a second set of records that will be used to perform the update operation.
504         List<Record> updateRecords =
505                 Arrays.asList(getCompleteSpeedRecord(), getCompleteSpeedRecord());
506 
507         // Modify the Uid of the updateRecords to the UUID that was present in the insert records,
508         // leaving out alternate records so that they have a new UUID which is not present in the
509         // dataBase.
510         for (int itr = 0; itr < updateRecords.size(); itr++) {
511             updateRecords.set(
512                     itr,
513                     getSpeedRecord_update(
514                             updateRecords.get(itr),
515                             itr % 2 == 0
516                                     ? insertedRecords.get(itr).getMetadata().getId()
517                                     : UUID.randomUUID().toString(),
518                             itr % 2 == 0
519                                     ? insertedRecords.get(itr).getMetadata().getId()
520                                     : UUID.randomUUID().toString()));
521         }
522 
523         try {
524             TestUtils.updateRecords(updateRecords);
525             Assert.fail("Expected to fail due to invalid records ids.");
526         } catch (HealthConnectException exception) {
527             assertThat(exception.getErrorCode())
528                     .isEqualTo(HealthConnectException.ERROR_INVALID_ARGUMENT);
529         }
530 
531         // assert the inserted data has not been modified by reading the data.
532         readSpeedRecordUsingIds(insertedRecords);
533     }
534 
535     @Test
testUpdateRecords_recordWithInvalidPackageName_noChangeInDataBase()536     public void testUpdateRecords_recordWithInvalidPackageName_noChangeInDataBase()
537             throws InterruptedException {
538         List<Record> insertedRecords =
539                 TestUtils.insertRecords(
540                         Arrays.asList(getCompleteSpeedRecord(), getCompleteSpeedRecord()));
541 
542         // read inserted records and verify that the data is same as inserted.
543         readSpeedRecordUsingIds(insertedRecords);
544 
545         // Generate a second set of records that will be used to perform the update operation.
546         List<Record> updateRecords =
547                 Arrays.asList(getCompleteSpeedRecord(), getCompleteSpeedRecord());
548 
549         // Modify the Uuid of the updateRecords to the uuid that was present in the insert records.
550         for (int itr = 0; itr < updateRecords.size(); itr++) {
551             updateRecords.set(
552                     itr,
553                     getSpeedRecord_update(
554                             updateRecords.get(itr),
555                             insertedRecords.get(itr).getMetadata().getId(),
556                             insertedRecords.get(itr).getMetadata().getClientRecordId()));
557             //             adding an entry with invalid packageName.
558             updateRecords.set(itr, getCompleteSpeedRecord());
559         }
560 
561         try {
562             TestUtils.updateRecords(updateRecords);
563             Assert.fail("Expected to fail due to invalid package.");
564         } catch (Exception exception) {
565             // verify that the testcase failed due to invalid argument exception.
566             assertThat(exception).isNotNull();
567         }
568 
569         // assert the inserted data has not been modified by reading the data.
570         readSpeedRecordUsingIds(insertedRecords);
571     }
572 
573     @Test
insertRecords_withDuplicatedClientRecordId_readNoDuplicates()574     public void insertRecords_withDuplicatedClientRecordId_readNoDuplicates() throws Exception {
575         int distinctRecordCount = 10;
576         List<SpeedRecord> records = new ArrayList<>();
577         Instant now = Instant.now();
578         for (int i = 0; i < distinctRecordCount; i++) {
579             SpeedRecord record =
580                     buildRecordForSpeed(
581                             /* speed= */ 10,
582                             /* millisFromStart= */ 0,
583                             /* startTime= */ now.minusMillis(i + 1),
584                             /* endTime= */ now.minusMillis(i),
585                             /* clientRecordId= */ "client_id_" + i);
586 
587             records.add(record);
588             records.add(record); // Add each record twice
589         }
590 
591         List<Record> insertedRecords = TestUtils.insertRecords(records);
592         assertThat(insertedRecords.size()).isEqualTo(records.size());
593 
594         List<Record> distinctRecords = distinctByUuid(insertedRecords);
595         assertThat(distinctRecords.size()).isEqualTo(distinctRecordCount);
596 
597         readSpeedRecordUsingIds(distinctRecords);
598     }
599 
600     @Test
insertRecords_sameClientRecordIdAndNewData_readNewData()601     public void insertRecords_sameClientRecordIdAndNewData_readNewData() throws Exception {
602         int recordCount = 10;
603         insertAndReadRecords(recordCount, /* speed= */ 10);
604 
605         double newSpeed = 20;
606         List<SpeedRecord> newRecords = insertAndReadRecords(recordCount, newSpeed);
607 
608         for (SpeedRecord record : newRecords) {
609             assertRecord(record, newSpeed);
610         }
611     }
612 
613     @Test
insertRecords_sameClientRecordIdAndNewerVersion_readNewData()614     public void insertRecords_sameClientRecordIdAndNewerVersion_readNewData() throws Exception {
615         int recordCount = 10;
616         long oldVersion = 0L;
617         double oldSpeed = 10;
618         insertAndReadRecords(recordCount, oldVersion, oldSpeed);
619 
620         long newVersion = 1L;
621         double newSpeed = 20;
622         List<SpeedRecord> newRecords = insertAndReadRecords(recordCount, newVersion, newSpeed);
623 
624         for (SpeedRecord record : newRecords) {
625             assertRecord(record, newSpeed);
626         }
627     }
628 
629     @Test
insertRecords_sameClientRecordIdAndSameVersion_readNewData()630     public void insertRecords_sameClientRecordIdAndSameVersion_readNewData() throws Exception {
631         int recordCount = 10;
632         long version = 1L;
633         double oldSpeed = 10;
634         insertAndReadRecords(recordCount, version, oldSpeed);
635 
636         double newSpeed = 20;
637         List<SpeedRecord> newRecords = insertAndReadRecords(recordCount, version, newSpeed);
638 
639         for (SpeedRecord record : newRecords) {
640             assertRecord(record, newSpeed);
641         }
642     }
643 
644     @Test
insertRecords_sameClientRecordIdAndOlderVersion_readOldData()645     public void insertRecords_sameClientRecordIdAndOlderVersion_readOldData() throws Exception {
646         int recordCount = 10;
647         long oldVersion = 1L;
648         double oldSpeed = 10;
649         insertAndReadRecords(recordCount, oldVersion, oldSpeed);
650 
651         long newVersion = 0L;
652         double newSpeed = 20;
653         List<SpeedRecord> newRecords = insertAndReadRecords(recordCount, newVersion, newSpeed);
654 
655         for (SpeedRecord record : newRecords) {
656             assertRecord(record, oldSpeed);
657         }
658     }
659 
660     @Test
updateRecords_byId_readNewData()661     public void updateRecords_byId_readNewData() throws Exception {
662         Instant now = Instant.now();
663         List<Record> insertedRecords =
664                 insertRecords(
665                         buildRecordForSpeed(1, now.minusMillis(2), now.minusMillis(1)),
666                         buildRecordForSpeed(2, now.minusMillis(3), now.minusMillis(2)),
667                         buildRecordForSpeed(3, now.minusMillis(4), now.minusMillis(3)));
668         List<String> insertedIds = getRecordIds(insertedRecords);
669 
670         List<Record> updatedRecords =
671                 List.of(
672                         buildRecordForSpeed(
673                                 insertedIds.get(0), 10, now.minusMillis(2), now.minusMillis(1)),
674                         buildRecordForSpeed(
675                                 insertedIds.get(1), 2, now.minusMillis(30), now.minusMillis(20)),
676                         buildRecordForSpeed(
677                                 insertedIds.get(2), 30, now.minusMillis(4), now.minusMillis(3)));
678         updateRecords(updatedRecords);
679 
680         readSpeedRecordUsingIds(updatedRecords);
681     }
682 
683     @Test
updateRecords_byClientRecordId_readNewData()684     public void updateRecords_byClientRecordId_readNewData() throws Exception {
685         Instant now = Instant.now();
686         List<Record> insertedRecords =
687                 insertRecords(
688                         buildRecordForSpeed(1, now.minusMillis(2), now.minusMillis(1), "id1"),
689                         buildRecordForSpeed(2, now.minusMillis(3), now.minusMillis(2), "id2"),
690                         buildRecordForSpeed(3, now.minusMillis(4), now.minusMillis(3), "id3"));
691 
692         List<SpeedRecord> updatedRecords =
693                 List.of(
694                         buildRecordForSpeed(10, now.minusMillis(2), now.minusMillis(1), "id1"),
695                         buildRecordForSpeed(2, now.minusMillis(30), now.minusMillis(20), "id2"),
696                         buildRecordForSpeed(30, now.minusMillis(4), now.minusMillis(3), "id3"));
697         updateRecords(updatedRecords);
698         copyRecordIdsViaReflection(insertedRecords, updatedRecords);
699 
700         readSpeedRecordUsingIds(updatedRecords);
701     }
702 
assertRecord(SpeedRecord record, double speed)703     private static void assertRecord(SpeedRecord record, double speed) {
704         assertThat(
705                         record.getSamples().stream()
706                                 .map(sample -> sample.getSpeed().getInMetersPerSecond())
707                                 .distinct()
708                                 .toList())
709                 .containsExactly(speed);
710     }
711 
insertAndReadRecords(int recordCount, double speed)712     private static List<SpeedRecord> insertAndReadRecords(int recordCount, double speed)
713             throws Exception {
714         return insertAndReadRecords(recordCount, /* version= */ 0L, speed);
715     }
716 
insertAndReadRecords( int recordCount, long version, double speed)717     private static List<SpeedRecord> insertAndReadRecords(
718             int recordCount, long version, double speed) throws Exception {
719         List<SpeedRecord> records = new ArrayList<>();
720         Instant now = Instant.now();
721         for (int i = 0; i < recordCount; i++) {
722             long millisFromStart = 0;
723             Instant startTime = now.minusSeconds(i + 1);
724             Instant endTime = now.minusSeconds(i);
725             String clientRecordId = "client_id_" + i;
726             records.add(
727                     buildRecordForSpeed(
728                             speed, millisFromStart, startTime, endTime, clientRecordId, version));
729         }
730         List<Record> insertedRecords = insertRecords(records);
731         assertThat(insertedRecords).hasSize(recordCount);
732 
733         List<SpeedRecord> readRecords =
734                 readRecords(
735                         new ReadRecordsRequestUsingFilters.Builder<>(SpeedRecord.class).build());
736         assertThat(readRecords).hasSize(recordCount);
737 
738         return readRecords;
739     }
740 
getSpeedRecord_update(Record record, String id, String clientRecordId)741     SpeedRecord getSpeedRecord_update(Record record, String id, String clientRecordId) {
742         Metadata metadata = record.getMetadata();
743         Metadata metadataWithId =
744                 new Metadata.Builder()
745                         .setId(id)
746                         .setClientRecordId(clientRecordId)
747                         .setClientRecordVersion(metadata.getClientRecordVersion())
748                         .setDataOrigin(metadata.getDataOrigin())
749                         .setDevice(metadata.getDevice())
750                         .setLastModifiedTime(metadata.getLastModifiedTime())
751                         .build();
752 
753         SpeedRecord.SpeedRecordSample speedRecordSample =
754                 new SpeedRecord.SpeedRecordSample(
755                         Velocity.fromMetersPerSecond(8.0), Instant.now().plusMillis(100));
756 
757         return new SpeedRecord.Builder(
758                         metadataWithId,
759                         Instant.now(),
760                         Instant.now().plusMillis(2000),
761                         List.of(speedRecordSample, speedRecordSample))
762                 .setStartZoneOffset(ZoneOffset.systemDefault().getRules().getOffset(Instant.now()))
763                 .setEndZoneOffset(ZoneOffset.systemDefault().getRules().getOffset(Instant.now()))
764                 .build();
765     }
766 
getBaseSpeedRecord()767     private static SpeedRecord getBaseSpeedRecord() {
768         SpeedRecord.SpeedRecordSample speedRecord =
769                 new SpeedRecord.SpeedRecordSample(
770                         Velocity.fromMetersPerSecond(10.0), Instant.now().plusMillis(100));
771         ArrayList<SpeedRecord.SpeedRecordSample> speedRecords = new ArrayList<>();
772         speedRecords.add(speedRecord);
773         speedRecords.add(speedRecord);
774 
775         return new SpeedRecord.Builder(
776                         new Metadata.Builder().build(),
777                         Instant.now(),
778                         Instant.now().plusMillis(1000),
779                         speedRecords)
780                 .build();
781     }
782 
getCompleteSpeedRecord()783     private static SpeedRecord getCompleteSpeedRecord() {
784         return buildRecordForSpeed(10, 100);
785     }
786 
buildRecordForSpeed(double speed, long millisFromStart)787     private static SpeedRecord buildRecordForSpeed(double speed, long millisFromStart) {
788         return buildRecordForSpeed(
789                 speed,
790                 millisFromStart,
791                 /* startTime= */ Instant.now(),
792                 /* endTime= */ Instant.now().plusMillis(1000),
793                 /* clientRecordId= */ "SPR" + Math.random());
794     }
795 
buildRecordForSpeed( double speed, long millisFromStart, Instant startTime, Instant endTime, String clientRecordId)796     private static SpeedRecord buildRecordForSpeed(
797             double speed,
798             long millisFromStart,
799             Instant startTime,
800             Instant endTime,
801             String clientRecordId) {
802         return buildRecordForSpeed(
803                 speed,
804                 millisFromStart,
805                 startTime,
806                 endTime,
807                 clientRecordId,
808                 /* clientRecordVersion= */ 0L);
809     }
810 
buildRecordForSpeed( double speed, Instant startTime, Instant endTime)811     private static SpeedRecord buildRecordForSpeed(
812             double speed, Instant startTime, Instant endTime) {
813         return buildRecordForSpeed(speed, startTime, endTime, /* clientRecordId= */ null);
814     }
815 
buildRecordForSpeed( double speed, Instant startTime, Instant endTime, String clientRecordId)816     private static SpeedRecord buildRecordForSpeed(
817             double speed, Instant startTime, Instant endTime, String clientRecordId) {
818         return buildRecordForSpeed(
819                 /* id= */ null,
820                 speed,
821                 /* millisFromStart= */ 0,
822                 startTime,
823                 endTime,
824                 clientRecordId,
825                 /* clientRecordVersion= */ 0L);
826     }
827 
buildRecordForSpeed( String id, double speed, Instant startTime, Instant endTime)828     private static SpeedRecord buildRecordForSpeed(
829             String id, double speed, Instant startTime, Instant endTime) {
830         return buildRecordForSpeed(
831                 id,
832                 speed,
833                 /* millisFromStart= */ 0,
834                 startTime,
835                 endTime,
836                 /* clientRecordId= */ null,
837                 /* clientRecordVersion= */ 0L);
838     }
839 
buildRecordForSpeed( double speed, long millisFromStart, Instant startTime, Instant endTime, String clientRecordId, long clientRecordVersion)840     private static SpeedRecord buildRecordForSpeed(
841             double speed,
842             long millisFromStart,
843             Instant startTime,
844             Instant endTime,
845             String clientRecordId,
846             long clientRecordVersion) {
847         return buildRecordForSpeed(
848                 null,
849                 speed,
850                 millisFromStart,
851                 startTime,
852                 endTime,
853                 clientRecordId,
854                 clientRecordVersion);
855     }
856 
buildRecordForSpeed( String id, double speed, long millisFromStart, Instant startTime, Instant endTime, String clientRecordId, long clientRecordVersion)857     private static SpeedRecord buildRecordForSpeed(
858             String id,
859             double speed,
860             long millisFromStart,
861             Instant startTime,
862             Instant endTime,
863             String clientRecordId,
864             long clientRecordVersion) {
865 
866         Device device =
867                 new Device.Builder()
868                         .setManufacturer("google")
869                         .setModel("Pixel4a")
870                         .setType(2)
871                         .build();
872         DataOrigin dataOrigin =
873                 new DataOrigin.Builder().setPackageName("android.healthconnect.cts").build();
874         Metadata.Builder testMetadataBuilder = new Metadata.Builder();
875         if (id != null) {
876             testMetadataBuilder.setId(id);
877         }
878         testMetadataBuilder.setDevice(device).setDataOrigin(dataOrigin);
879         testMetadataBuilder.setClientRecordId(clientRecordId);
880         testMetadataBuilder.setClientRecordVersion(clientRecordVersion);
881         testMetadataBuilder.setRecordingMethod(Metadata.RECORDING_METHOD_ACTIVELY_RECORDED);
882 
883         SpeedRecord.SpeedRecordSample speedRecord =
884                 new SpeedRecord.SpeedRecordSample(
885                         Velocity.fromMetersPerSecond(speed), startTime.plusMillis(millisFromStart));
886 
887         ArrayList<SpeedRecord.SpeedRecordSample> speedRecords = new ArrayList<>();
888         speedRecords.add(speedRecord);
889         speedRecords.add(speedRecord);
890 
891         return new SpeedRecord.Builder(
892                         testMetadataBuilder.build(), startTime, endTime, speedRecords)
893                 .build();
894     }
895 }
896