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.utils;
18 
19 import static android.Manifest.permission.READ_DEVICE_CONFIG;
20 import static android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG;
21 import static android.health.connect.HealthDataCategory.ACTIVITY;
22 import static android.health.connect.HealthDataCategory.BODY_MEASUREMENTS;
23 import static android.health.connect.HealthDataCategory.CYCLE_TRACKING;
24 import static android.health.connect.HealthDataCategory.NUTRITION;
25 import static android.health.connect.HealthDataCategory.SLEEP;
26 import static android.health.connect.HealthDataCategory.VITALS;
27 import static android.health.connect.HealthPermissionCategory.BASAL_METABOLIC_RATE;
28 import static android.health.connect.HealthPermissionCategory.EXERCISE;
29 import static android.health.connect.HealthPermissionCategory.HEART_RATE;
30 import static android.health.connect.HealthPermissionCategory.PLANNED_EXERCISE;
31 import static android.health.connect.HealthPermissionCategory.STEPS;
32 import static android.healthconnect.cts.utils.DataFactory.NOW;
33 import static android.healthconnect.cts.utils.DataFactory.getDataOrigin;
34 import static android.healthconnect.cts.utils.PermissionHelper.MANAGE_HEALTH_DATA;
35 import static android.healthconnect.test.app.TestAppReceiver.ACTION_AGGREGATE_STEPS_COUNT;
36 import static android.healthconnect.test.app.TestAppReceiver.ACTION_INSERT_EXERCISE_RECORD;
37 import static android.healthconnect.test.app.TestAppReceiver.ACTION_INSERT_PLANNED_EXERCISE_RECORD;
38 import static android.healthconnect.test.app.TestAppReceiver.ACTION_INSERT_STEPS_RECORDS;
39 import static android.healthconnect.test.app.TestAppReceiver.ACTION_INSERT_WEIGHT_RECORDS;
40 import static android.healthconnect.test.app.TestAppReceiver.ACTION_READ_STEPS_RECORDS_USING_FILTERS;
41 import static android.healthconnect.test.app.TestAppReceiver.ACTION_READ_STEPS_RECORDS_USING_RECORD_IDS;
42 import static android.healthconnect.test.app.TestAppReceiver.EXTRA_END_TIMES;
43 import static android.healthconnect.test.app.TestAppReceiver.EXTRA_PACKAGE_NAMES;
44 import static android.healthconnect.test.app.TestAppReceiver.EXTRA_PLANNED_EXERCISE_SESSION_ID;
45 import static android.healthconnect.test.app.TestAppReceiver.EXTRA_RECORD_CLIENT_IDS;
46 import static android.healthconnect.test.app.TestAppReceiver.EXTRA_RECORD_IDS;
47 import static android.healthconnect.test.app.TestAppReceiver.EXTRA_RECORD_VALUES;
48 import static android.healthconnect.test.app.TestAppReceiver.EXTRA_SENDER_PACKAGE_NAME;
49 import static android.healthconnect.test.app.TestAppReceiver.EXTRA_TIMES;
50 
51 import static com.android.compatibility.common.util.FeatureUtil.AUTOMOTIVE_FEATURE;
52 import static com.android.compatibility.common.util.FeatureUtil.hasSystemFeature;
53 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
54 
55 import static com.google.common.truth.Truth.assertThat;
56 
57 import static java.util.Collections.unmodifiableList;
58 import static java.util.Objects.requireNonNull;
59 
60 import android.app.UiAutomation;
61 import android.content.Context;
62 import android.content.Intent;
63 import android.content.pm.PackageManager;
64 import android.health.connect.AggregateRecordsGroupedByDurationResponse;
65 import android.health.connect.AggregateRecordsGroupedByPeriodResponse;
66 import android.health.connect.AggregateRecordsRequest;
67 import android.health.connect.AggregateRecordsResponse;
68 import android.health.connect.ApplicationInfoResponse;
69 import android.health.connect.CreateMedicalDataSourceRequest;
70 import android.health.connect.DeleteUsingFiltersRequest;
71 import android.health.connect.FetchDataOriginsPriorityOrderResponse;
72 import android.health.connect.HealthConnectDataState;
73 import android.health.connect.HealthConnectException;
74 import android.health.connect.HealthConnectManager;
75 import android.health.connect.HealthPermissionCategory;
76 import android.health.connect.InsertRecordsResponse;
77 import android.health.connect.MedicalIdFilter;
78 import android.health.connect.ReadMedicalResourcesRequest;
79 import android.health.connect.ReadMedicalResourcesResponse;
80 import android.health.connect.ReadRecordsRequest;
81 import android.health.connect.ReadRecordsRequestUsingFilters;
82 import android.health.connect.ReadRecordsRequestUsingIds;
83 import android.health.connect.ReadRecordsResponse;
84 import android.health.connect.RecordIdFilter;
85 import android.health.connect.RecordTypeInfoResponse;
86 import android.health.connect.TimeInstantRangeFilter;
87 import android.health.connect.UpdateDataOriginPriorityOrderRequest;
88 import android.health.connect.accesslog.AccessLog;
89 import android.health.connect.changelog.ChangeLogTokenRequest;
90 import android.health.connect.changelog.ChangeLogTokenResponse;
91 import android.health.connect.changelog.ChangeLogsRequest;
92 import android.health.connect.changelog.ChangeLogsResponse;
93 import android.health.connect.datatypes.ActiveCaloriesBurnedRecord;
94 import android.health.connect.datatypes.AppInfo;
95 import android.health.connect.datatypes.BasalBodyTemperatureRecord;
96 import android.health.connect.datatypes.BasalMetabolicRateRecord;
97 import android.health.connect.datatypes.BloodGlucoseRecord;
98 import android.health.connect.datatypes.BloodPressureRecord;
99 import android.health.connect.datatypes.BodyFatRecord;
100 import android.health.connect.datatypes.BodyTemperatureRecord;
101 import android.health.connect.datatypes.BodyWaterMassRecord;
102 import android.health.connect.datatypes.BoneMassRecord;
103 import android.health.connect.datatypes.CervicalMucusRecord;
104 import android.health.connect.datatypes.CyclingPedalingCadenceRecord;
105 import android.health.connect.datatypes.DataOrigin;
106 import android.health.connect.datatypes.DistanceRecord;
107 import android.health.connect.datatypes.ElevationGainedRecord;
108 import android.health.connect.datatypes.ExerciseSessionRecord;
109 import android.health.connect.datatypes.FloorsClimbedRecord;
110 import android.health.connect.datatypes.HeartRateRecord;
111 import android.health.connect.datatypes.HeartRateVariabilityRmssdRecord;
112 import android.health.connect.datatypes.HeightRecord;
113 import android.health.connect.datatypes.HydrationRecord;
114 import android.health.connect.datatypes.IntermenstrualBleedingRecord;
115 import android.health.connect.datatypes.LeanBodyMassRecord;
116 import android.health.connect.datatypes.MedicalDataSource;
117 import android.health.connect.datatypes.MedicalResource;
118 import android.health.connect.datatypes.MenstruationFlowRecord;
119 import android.health.connect.datatypes.MenstruationPeriodRecord;
120 import android.health.connect.datatypes.Metadata;
121 import android.health.connect.datatypes.NutritionRecord;
122 import android.health.connect.datatypes.OvulationTestRecord;
123 import android.health.connect.datatypes.OxygenSaturationRecord;
124 import android.health.connect.datatypes.PlannedExerciseSessionRecord;
125 import android.health.connect.datatypes.PowerRecord;
126 import android.health.connect.datatypes.Record;
127 import android.health.connect.datatypes.RespiratoryRateRecord;
128 import android.health.connect.datatypes.RestingHeartRateRecord;
129 import android.health.connect.datatypes.SexualActivityRecord;
130 import android.health.connect.datatypes.SkinTemperatureRecord;
131 import android.health.connect.datatypes.SleepSessionRecord;
132 import android.health.connect.datatypes.SpeedRecord;
133 import android.health.connect.datatypes.StepsCadenceRecord;
134 import android.health.connect.datatypes.StepsRecord;
135 import android.health.connect.datatypes.TotalCaloriesBurnedRecord;
136 import android.health.connect.datatypes.Vo2MaxRecord;
137 import android.health.connect.datatypes.WeightRecord;
138 import android.health.connect.datatypes.WheelchairPushesRecord;
139 import android.health.connect.migration.MigrationEntity;
140 import android.health.connect.migration.MigrationException;
141 import android.healthconnect.test.app.TestAppReceiver;
142 import android.os.Bundle;
143 import android.os.OutcomeReceiver;
144 import android.os.ParcelFileDescriptor;
145 import android.provider.DeviceConfig;
146 import android.util.Log;
147 
148 import androidx.annotation.NonNull;
149 import androidx.test.core.app.ApplicationProvider;
150 import androidx.test.platform.app.InstrumentationRegistry;
151 
152 import java.io.BufferedReader;
153 import java.io.FileInputStream;
154 import java.io.FileNotFoundException;
155 import java.io.IOException;
156 import java.io.InputStreamReader;
157 import java.lang.reflect.Field;
158 import java.time.Duration;
159 import java.time.Instant;
160 import java.time.LocalDate;
161 import java.time.LocalTime;
162 import java.time.Period;
163 import java.time.ZoneOffset;
164 import java.time.temporal.ChronoUnit;
165 import java.util.ArrayList;
166 import java.util.Arrays;
167 import java.util.Collection;
168 import java.util.Collections;
169 import java.util.HashMap;
170 import java.util.List;
171 import java.util.Map;
172 import java.util.Set;
173 import java.util.concurrent.ConcurrentHashMap;
174 import java.util.concurrent.CountDownLatch;
175 import java.util.concurrent.Executors;
176 import java.util.concurrent.TimeUnit;
177 import java.util.concurrent.atomic.AtomicReference;
178 import java.util.function.Consumer;
179 import java.util.function.Predicate;
180 import java.util.stream.Collectors;
181 import java.util.stream.IntStream;
182 
183 public final class TestUtils {
184     private static final String TAG = "HCTestUtils";
185     private static final int TIMEOUT_SECONDS = 5;
186 
187     public static final String PKG_TEST_APP = "android.healthconnect.test.app";
188     private static final String TEST_APP_RECEIVER =
189             PKG_TEST_APP + "." + TestAppReceiver.class.getSimpleName();
190 
isHardwareAutomotive()191     public static boolean isHardwareAutomotive() {
192         return hasSystemFeature(AUTOMOTIVE_FEATURE);
193     }
194 
getChangeLogToken(ChangeLogTokenRequest request)195     public static ChangeLogTokenResponse getChangeLogToken(ChangeLogTokenRequest request)
196             throws InterruptedException {
197         return getChangeLogToken(request, ApplicationProvider.getApplicationContext());
198     }
199 
getChangeLogToken( ChangeLogTokenRequest request, Context context)200     public static ChangeLogTokenResponse getChangeLogToken(
201             ChangeLogTokenRequest request, Context context) throws InterruptedException {
202         HealthConnectReceiver<ChangeLogTokenResponse> receiver = new HealthConnectReceiver<>();
203         getHealthConnectManager(context)
204                 .getChangeLogToken(request, Executors.newSingleThreadExecutor(), receiver);
205         return receiver.getResponse();
206     }
207 
insertRecordAndGetId(Record record)208     public static String insertRecordAndGetId(Record record) throws InterruptedException {
209         return insertRecords(Collections.singletonList(record)).get(0).getMetadata().getId();
210     }
211 
insertRecordAndGetId(Record record, Context context)212     public static String insertRecordAndGetId(Record record, Context context)
213             throws InterruptedException {
214         return insertRecords(Collections.singletonList(record), context)
215                 .get(0)
216                 .getMetadata()
217                 .getId();
218     }
219 
220     /**
221      * Insert record to the database.
222      *
223      * @param record record to insert
224      * @return inserted record
225      */
insertRecord(Record record)226     public static Record insertRecord(Record record) throws InterruptedException {
227         return insertRecords(Collections.singletonList(record)).get(0);
228     }
229 
230     /**
231      * Inserts records to the database.
232      *
233      * @param records records to insert
234      * @return inserted records
235      */
insertRecords(List<? extends Record> records)236     public static List<Record> insertRecords(List<? extends Record> records)
237             throws InterruptedException {
238         return insertRecords(records, ApplicationProvider.getApplicationContext());
239     }
240 
241     /**
242      * Inserts records to the database.
243      *
244      * @param records records to insert
245      * @return inserted records
246      */
insertRecords(Record... records)247     public static List<Record> insertRecords(Record... records) throws InterruptedException {
248         return insertRecords(Arrays.asList(records), ApplicationProvider.getApplicationContext());
249     }
250 
251     /**
252      * Inserts records to the database.
253      *
254      * @param records records to insert.
255      * @param context a {@link Context} to obtain {@link HealthConnectManager}.
256      * @return inserted records.
257      */
insertRecords(List<? extends Record> records, Context context)258     public static List<Record> insertRecords(List<? extends Record> records, Context context)
259             throws InterruptedException {
260         HealthConnectReceiver<InsertRecordsResponse> receiver = new HealthConnectReceiver<>();
261         getHealthConnectManager(context)
262                 .insertRecords(
263                         unmodifiableList(records), Executors.newSingleThreadExecutor(), receiver);
264         List<Record> returnedRecords = receiver.getResponse().getRecords();
265         assertThat(returnedRecords).hasSize(records.size());
266         return returnedRecords;
267     }
268 
269     /**
270      * Returns all records from the `records` list in their original order, but distinct by UUID.
271      */
distinctByUuid(List<T> records)272     public static <T extends Record> List<T> distinctByUuid(List<T> records) {
273         return records.stream().filter(distinctByUuid()).toList();
274     }
275 
distinctByUuid()276     private static Predicate<? super Record> distinctByUuid() {
277         Set<String> seen = ConcurrentHashMap.newKeySet();
278         return record -> seen.add(record.getMetadata().getId());
279     }
280 
281     /** Updates the provided records in the database. */
updateRecords(List<? extends Record> records)282     public static void updateRecords(List<? extends Record> records) throws InterruptedException {
283         updateRecords(records, ApplicationProvider.getApplicationContext());
284     }
285 
286     /** Synchronously updates records in HC. */
updateRecords(List<? extends Record> records, Context context)287     public static void updateRecords(List<? extends Record> records, Context context)
288             throws InterruptedException {
289         HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
290         getHealthConnectManager(context)
291                 .updateRecords(
292                         unmodifiableList(records), Executors.newSingleThreadExecutor(), receiver);
293         receiver.verifyNoExceptionOrThrow();
294     }
295 
getChangeLogs(ChangeLogsRequest changeLogsRequest)296     public static ChangeLogsResponse getChangeLogs(ChangeLogsRequest changeLogsRequest)
297             throws InterruptedException {
298         return getChangeLogs(changeLogsRequest, ApplicationProvider.getApplicationContext());
299     }
300 
getChangeLogs( ChangeLogsRequest changeLogsRequest, Context context)301     public static ChangeLogsResponse getChangeLogs(
302             ChangeLogsRequest changeLogsRequest, Context context) throws InterruptedException {
303         HealthConnectReceiver<ChangeLogsResponse> receiver = new HealthConnectReceiver<>();
304         getHealthConnectManager(context)
305                 .getChangeLogs(changeLogsRequest, Executors.newSingleThreadExecutor(), receiver);
306         return receiver.getResponse();
307     }
308 
getAggregateResponse( AggregateRecordsRequest<T> request)309     public static <T> AggregateRecordsResponse<T> getAggregateResponse(
310             AggregateRecordsRequest<T> request) throws InterruptedException {
311         HealthConnectReceiver<AggregateRecordsResponse<T>> receiver =
312                 new HealthConnectReceiver<AggregateRecordsResponse<T>>();
313         getHealthConnectManager().aggregate(request, Executors.newSingleThreadExecutor(), receiver);
314         return receiver.getResponse();
315     }
316 
getAggregateResponse( AggregateRecordsRequest<T> request, List<Record> recordsToInsert)317     public static <T> AggregateRecordsResponse<T> getAggregateResponse(
318             AggregateRecordsRequest<T> request, List<Record> recordsToInsert)
319             throws InterruptedException {
320         if (recordsToInsert != null) {
321             insertRecords(recordsToInsert);
322         }
323 
324         HealthConnectReceiver<AggregateRecordsResponse<T>> receiver = new HealthConnectReceiver<>();
325         getHealthConnectManager().aggregate(request, Executors.newSingleThreadExecutor(), receiver);
326         return receiver.getResponse();
327     }
328 
329     public static <T>
getAggregateResponseGroupByDuration( AggregateRecordsRequest<T> request, Duration duration)330             List<AggregateRecordsGroupedByDurationResponse<T>> getAggregateResponseGroupByDuration(
331                     AggregateRecordsRequest<T> request, Duration duration)
332                     throws InterruptedException {
333         HealthConnectReceiver<List<AggregateRecordsGroupedByDurationResponse<T>>> receiver =
334                 new HealthConnectReceiver<>();
335         getHealthConnectManager()
336                 .aggregateGroupByDuration(
337                         request, duration, Executors.newSingleThreadExecutor(), receiver);
338         return receiver.getResponse();
339     }
340 
341     public static <T>
getAggregateResponseGroupByPeriod( AggregateRecordsRequest<T> request, Period period)342             List<AggregateRecordsGroupedByPeriodResponse<T>> getAggregateResponseGroupByPeriod(
343                     AggregateRecordsRequest<T> request, Period period) throws InterruptedException {
344         HealthConnectReceiver<List<AggregateRecordsGroupedByPeriodResponse<T>>> receiver =
345                 new HealthConnectReceiver<>();
346         getHealthConnectManager()
347                 .aggregateGroupByPeriod(
348                         request, period, Executors.newSingleThreadExecutor(), receiver);
349         return receiver.getResponse();
350     }
351 
readRecords(ReadRecordsRequest<T> request)352     public static <T extends Record> List<T> readRecords(ReadRecordsRequest<T> request)
353             throws InterruptedException {
354         return getReadRecordsResponse(request).getRecords();
355     }
356 
readRecords( ReadRecordsRequest<T> request, Context context)357     public static <T extends Record> List<T> readRecords(
358             ReadRecordsRequest<T> request, Context context) throws InterruptedException {
359         return getReadRecordsResponse(request, context).getRecords();
360     }
361 
getReadRecordsResponse( ReadRecordsRequest<T> request)362     public static <T extends Record> ReadRecordsResponse<T> getReadRecordsResponse(
363             ReadRecordsRequest<T> request) throws InterruptedException {
364         return getReadRecordsResponse(request, ApplicationProvider.getApplicationContext());
365     }
366 
getReadRecordsResponse( ReadRecordsRequest<T> request, Context context)367     public static <T extends Record> ReadRecordsResponse<T> getReadRecordsResponse(
368             ReadRecordsRequest<T> request, Context context) throws InterruptedException {
369         assertThat(request.getRecordType()).isNotNull();
370         HealthConnectReceiver<ReadRecordsResponse<T>> receiver = new HealthConnectReceiver<>();
371         getHealthConnectManager(context)
372                 .readRecords(request, Executors.newSingleThreadExecutor(), receiver);
373         return receiver.getResponse();
374     }
375 
assertRecordNotFound(String uuid, Class<T> recordType)376     public static <T extends Record> void assertRecordNotFound(String uuid, Class<T> recordType)
377             throws InterruptedException {
378         assertThat(
379                         readRecords(
380                                 new ReadRecordsRequestUsingIds.Builder<>(recordType)
381                                         .addId(uuid)
382                                         .build()))
383                 .isEmpty();
384     }
385 
assertRecordFound(String uuid, Class<T> recordType)386     public static <T extends Record> void assertRecordFound(String uuid, Class<T> recordType)
387             throws InterruptedException {
388         assertThat(
389                         readRecords(
390                                 new ReadRecordsRequestUsingIds.Builder<>(recordType)
391                                         .addId(uuid)
392                                         .build()))
393                 .isNotEmpty();
394     }
395 
396     /** Reads all records in the DB for a given {@code recordClass}. */
readAllRecords(Class<T> recordClass)397     public static <T extends Record> List<T> readAllRecords(Class<T> recordClass)
398             throws InterruptedException {
399         List<T> records = new ArrayList<>();
400         ReadRecordsResponse<T> readRecordsResponse =
401                 readRecordsWithPagination(
402                         new ReadRecordsRequestUsingFilters.Builder<>(recordClass).build());
403         while (true) {
404             records.addAll(readRecordsResponse.getRecords());
405             long pageToken = readRecordsResponse.getNextPageToken();
406             if (pageToken == -1) {
407                 break;
408             }
409             readRecordsResponse =
410                     readRecordsWithPagination(
411                             new ReadRecordsRequestUsingFilters.Builder<>(recordClass)
412                                     .setPageToken(pageToken)
413                                     .build());
414         }
415         return records;
416     }
417 
readRecordsWithPagination( ReadRecordsRequest<T> request)418     public static <T extends Record> ReadRecordsResponse<T> readRecordsWithPagination(
419             ReadRecordsRequest<T> request) throws InterruptedException {
420         HealthConnectReceiver<ReadRecordsResponse<T>> receiver = new HealthConnectReceiver<>();
421         getHealthConnectManager()
422                 .readRecords(request, Executors.newSingleThreadExecutor(), receiver);
423         return receiver.getResponse();
424     }
425 
setAutoDeletePeriod(int period)426     public static void setAutoDeletePeriod(int period) throws InterruptedException {
427         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
428         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
429         try {
430             HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
431             getHealthConnectManager()
432                     .setRecordRetentionPeriodInDays(
433                             period, Executors.newSingleThreadExecutor(), receiver);
434             receiver.verifyNoExceptionOrThrow();
435         } finally {
436             uiAutomation.dropShellPermissionIdentity();
437         }
438     }
439 
verifyDeleteRecords(DeleteUsingFiltersRequest request)440     public static void verifyDeleteRecords(DeleteUsingFiltersRequest request)
441             throws InterruptedException {
442         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
443         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
444         try {
445             HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
446             getHealthConnectManager()
447                     .deleteRecords(request, Executors.newSingleThreadExecutor(), receiver);
448             receiver.verifyNoExceptionOrThrow();
449         } finally {
450             uiAutomation.dropShellPermissionIdentity();
451         }
452     }
453 
verifyDeleteRecords(List<RecordIdFilter> request)454     public static void verifyDeleteRecords(List<RecordIdFilter> request)
455             throws InterruptedException {
456         verifyDeleteRecords(request, ApplicationProvider.getApplicationContext());
457     }
458 
verifyDeleteRecords(List<RecordIdFilter> request, Context context)459     public static void verifyDeleteRecords(List<RecordIdFilter> request, Context context)
460             throws InterruptedException {
461         HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
462         getHealthConnectManager(context)
463                 .deleteRecords(request, Executors.newSingleThreadExecutor(), receiver);
464         receiver.verifyNoExceptionOrThrow();
465     }
466 
verifyDeleteRecords( Class<? extends Record> recordType, TimeInstantRangeFilter timeRangeFilter)467     public static void verifyDeleteRecords(
468             Class<? extends Record> recordType, TimeInstantRangeFilter timeRangeFilter)
469             throws InterruptedException {
470         HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
471         getHealthConnectManager()
472                 .deleteRecords(
473                         recordType, timeRangeFilter, Executors.newSingleThreadExecutor(), receiver);
474         receiver.verifyNoExceptionOrThrow();
475     }
476 
477     /** Helper function to delete records from the DB using HealthConnectManager. */
deleteRecords(List<? extends Record> records)478     public static void deleteRecords(List<? extends Record> records) throws InterruptedException {
479         List<RecordIdFilter> recordIdFilters =
480                 records.stream()
481                         .map(
482                                 (record ->
483                                         RecordIdFilter.fromId(
484                                                 record.getClass(), record.getMetadata().getId())))
485                         .collect(Collectors.toList());
486         verifyDeleteRecords(recordIdFilters);
487     }
488 
489     /** Helper function to delete records from the DB, using HealthConnectManager. */
deleteRecordsByIdFilter(List<RecordIdFilter> recordIdFilters)490     public static void deleteRecordsByIdFilter(List<RecordIdFilter> recordIdFilters)
491             throws InterruptedException {
492         verifyDeleteRecords(recordIdFilters);
493     }
494 
queryAccessLogs()495     public static List<AccessLog> queryAccessLogs() throws InterruptedException {
496         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
497         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
498         try {
499             HealthConnectReceiver<List<AccessLog>> receiver = new HealthConnectReceiver<>();
500             getHealthConnectManager()
501                     .queryAccessLogs(Executors.newSingleThreadExecutor(), receiver);
502             return receiver.getResponse();
503         } finally {
504             uiAutomation.dropShellPermissionIdentity();
505         }
506     }
507 
queryAllRecordTypesInfo()508     public static Map<Class<? extends Record>, RecordTypeInfoResponse> queryAllRecordTypesInfo()
509             throws InterruptedException, NullPointerException {
510         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
511         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
512         try {
513             HealthConnectReceiver<Map<Class<? extends Record>, RecordTypeInfoResponse>> receiver =
514                     new HealthConnectReceiver<>();
515             getHealthConnectManager()
516                     .queryAllRecordTypesInfo(Executors.newSingleThreadExecutor(), receiver);
517             return receiver.getResponse();
518         } finally {
519             uiAutomation.dropShellPermissionIdentity();
520         }
521     }
522 
getActivityDates(List<Class<? extends Record>> recordTypes)523     public static List<LocalDate> getActivityDates(List<Class<? extends Record>> recordTypes)
524             throws InterruptedException {
525         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
526         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
527         try {
528             HealthConnectReceiver<List<LocalDate>> receiver = new HealthConnectReceiver<>();
529             getHealthConnectManager()
530                     .queryActivityDates(recordTypes, Executors.newSingleThreadExecutor(), receiver);
531             return receiver.getResponse();
532         } finally {
533             uiAutomation.dropShellPermissionIdentity();
534         }
535     }
536 
startMigration()537     public static void startMigration() throws InterruptedException {
538         MigrationReceiver receiver = new MigrationReceiver();
539         getHealthConnectManager().startMigration(Executors.newSingleThreadExecutor(), receiver);
540         receiver.verifyNoExceptionOrThrow();
541     }
542 
writeMigrationData(List<MigrationEntity> entities)543     public static void writeMigrationData(List<MigrationEntity> entities)
544             throws InterruptedException {
545         MigrationReceiver receiver = new MigrationReceiver();
546         getHealthConnectManager()
547                 .writeMigrationData(entities, Executors.newSingleThreadExecutor(), receiver);
548         receiver.verifyNoExceptionOrThrow();
549     }
550 
finishMigration()551     public static void finishMigration() throws InterruptedException {
552         MigrationReceiver receiver = new MigrationReceiver();
553         getHealthConnectManager().finishMigration(Executors.newSingleThreadExecutor(), receiver);
554         receiver.verifyNoExceptionOrThrow();
555     }
556 
insertMinDataMigrationSdkExtensionVersion(int version)557     public static void insertMinDataMigrationSdkExtensionVersion(int version)
558             throws InterruptedException {
559         MigrationReceiver receiver = new MigrationReceiver();
560         getHealthConnectManager()
561                 .insertMinDataMigrationSdkExtensionVersion(
562                         version, Executors.newSingleThreadExecutor(), receiver);
563         receiver.verifyNoExceptionOrThrow();
564     }
565 
deleteAllStagedRemoteData()566     public static void deleteAllStagedRemoteData() {
567         HealthConnectManager service = getHealthConnectManager();
568         runWithShellPermissionIdentity(
569                 () ->
570                         // TODO(b/241542162): Avoid reflection once TestApi can be called from CTS
571                         service.getClass().getMethod("deleteAllStagedRemoteData").invoke(service),
572                 "android.permission.DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA");
573     }
574 
575     /** Set lower rate limits for testing */
setLowerRateLimitsForTesting(boolean enabled)576     public static boolean setLowerRateLimitsForTesting(boolean enabled) {
577         HealthConnectManager service = getHealthConnectManager();
578         try {
579             runWithShellPermissionIdentity(
580                     () ->
581                             // TODO(b/241542162): Avoid reflection once TestApi can be called from
582                             // CTS
583                             service.getClass()
584                                     .getMethod("setLowerRateLimitsForTesting", boolean.class)
585                                     .invoke(service, enabled),
586                     "android.permission.DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA");
587             return true;
588         } catch (RuntimeException e) {
589             // Old versions of the module don't have this API.
590             Log.e(TAG, "Couldn't override quota for testing", e);
591             return false;
592         }
593     }
594 
getHealthConnectDataMigrationState()595     public static int getHealthConnectDataMigrationState() throws InterruptedException {
596         HealthConnectReceiver<HealthConnectDataState> receiver = new HealthConnectReceiver<>();
597         getHealthConnectManager()
598                 .getHealthConnectDataState(Executors.newSingleThreadExecutor(), receiver);
599         return receiver.getResponse().getDataMigrationState();
600     }
601 
getHealthConnectDataRestoreState()602     public static int getHealthConnectDataRestoreState() throws InterruptedException {
603         HealthConnectReceiver<HealthConnectDataState> receiver = new HealthConnectReceiver<>();
604         runWithShellPermissionIdentity(
605                 () ->
606                         getHealthConnectManager()
607                                 .getHealthConnectDataState(
608                                         Executors.newSingleThreadExecutor(), receiver),
609                 MANAGE_HEALTH_DATA);
610         return receiver.getResponse().getDataRestoreState();
611     }
612 
getApplicationInfo()613     public static List<AppInfo> getApplicationInfo() throws InterruptedException {
614         HealthConnectReceiver<ApplicationInfoResponse> receiver = new HealthConnectReceiver<>();
615         getHealthConnectManager()
616                 .getContributorApplicationsInfo(Executors.newSingleThreadExecutor(), receiver);
617         return receiver.getResponse().getApplicationInfoList();
618     }
619 
getRecordById(List<T> list, String id)620     public static <T extends Record> T getRecordById(List<T> list, String id) {
621         for (T record : list) {
622             if (record.getMetadata().getId().equals(id)) {
623                 return record;
624             }
625         }
626 
627         throw new AssertionError("Record not found with id: " + id);
628     }
629 
populateAndResetExpectedResponseMap( HashMap<Class<? extends Record>, RecordTypeInfoTestResponse> expectedResponseMap)630     public static void populateAndResetExpectedResponseMap(
631             HashMap<Class<? extends Record>, RecordTypeInfoTestResponse> expectedResponseMap) {
632         expectedResponseMap.put(
633                 ElevationGainedRecord.class,
634                 new RecordTypeInfoTestResponse(
635                         ACTIVITY, HealthPermissionCategory.ELEVATION_GAINED, new ArrayList<>()));
636         expectedResponseMap.put(
637                 OvulationTestRecord.class,
638                 new RecordTypeInfoTestResponse(
639                         CYCLE_TRACKING,
640                         HealthPermissionCategory.OVULATION_TEST,
641                         new ArrayList<>()));
642         expectedResponseMap.put(
643                 DistanceRecord.class,
644                 new RecordTypeInfoTestResponse(
645                         ACTIVITY, HealthPermissionCategory.DISTANCE, new ArrayList<>()));
646         expectedResponseMap.put(
647                 SpeedRecord.class,
648                 new RecordTypeInfoTestResponse(
649                         ACTIVITY, HealthPermissionCategory.SPEED, new ArrayList<>()));
650 
651         expectedResponseMap.put(
652                 Vo2MaxRecord.class,
653                 new RecordTypeInfoTestResponse(
654                         ACTIVITY, HealthPermissionCategory.VO2_MAX, new ArrayList<>()));
655         expectedResponseMap.put(
656                 OxygenSaturationRecord.class,
657                 new RecordTypeInfoTestResponse(
658                         VITALS, HealthPermissionCategory.OXYGEN_SATURATION, new ArrayList<>()));
659         expectedResponseMap.put(
660                 TotalCaloriesBurnedRecord.class,
661                 new RecordTypeInfoTestResponse(
662                         ACTIVITY,
663                         HealthPermissionCategory.TOTAL_CALORIES_BURNED,
664                         new ArrayList<>()));
665         expectedResponseMap.put(
666                 HydrationRecord.class,
667                 new RecordTypeInfoTestResponse(
668                         NUTRITION, HealthPermissionCategory.HYDRATION, new ArrayList<>()));
669         expectedResponseMap.put(
670                 StepsRecord.class,
671                 new RecordTypeInfoTestResponse(ACTIVITY, STEPS, new ArrayList<>()));
672         expectedResponseMap.put(
673                 CervicalMucusRecord.class,
674                 new RecordTypeInfoTestResponse(
675                         CYCLE_TRACKING,
676                         HealthPermissionCategory.CERVICAL_MUCUS,
677                         new ArrayList<>()));
678         expectedResponseMap.put(
679                 ExerciseSessionRecord.class,
680                 new RecordTypeInfoTestResponse(ACTIVITY, EXERCISE, new ArrayList<>()));
681         expectedResponseMap.put(
682                 HeartRateRecord.class,
683                 new RecordTypeInfoTestResponse(VITALS, HEART_RATE, new ArrayList<>()));
684         expectedResponseMap.put(
685                 RespiratoryRateRecord.class,
686                 new RecordTypeInfoTestResponse(
687                         VITALS, HealthPermissionCategory.RESPIRATORY_RATE, new ArrayList<>()));
688         expectedResponseMap.put(
689                 BasalBodyTemperatureRecord.class,
690                 new RecordTypeInfoTestResponse(
691                         VITALS,
692                         HealthPermissionCategory.BASAL_BODY_TEMPERATURE,
693                         new ArrayList<>()));
694         expectedResponseMap.put(
695                 WheelchairPushesRecord.class,
696                 new RecordTypeInfoTestResponse(
697                         ACTIVITY, HealthPermissionCategory.WHEELCHAIR_PUSHES, new ArrayList<>()));
698         expectedResponseMap.put(
699                 PowerRecord.class,
700                 new RecordTypeInfoTestResponse(
701                         ACTIVITY, HealthPermissionCategory.POWER, new ArrayList<>()));
702         expectedResponseMap.put(
703                 BodyWaterMassRecord.class,
704                 new RecordTypeInfoTestResponse(
705                         BODY_MEASUREMENTS,
706                         HealthPermissionCategory.BODY_WATER_MASS,
707                         new ArrayList<>()));
708         expectedResponseMap.put(
709                 WeightRecord.class,
710                 new RecordTypeInfoTestResponse(
711                         BODY_MEASUREMENTS, HealthPermissionCategory.WEIGHT, new ArrayList<>()));
712         expectedResponseMap.put(
713                 BoneMassRecord.class,
714                 new RecordTypeInfoTestResponse(
715                         BODY_MEASUREMENTS, HealthPermissionCategory.BONE_MASS, new ArrayList<>()));
716         expectedResponseMap.put(
717                 RestingHeartRateRecord.class,
718                 new RecordTypeInfoTestResponse(
719                         VITALS, HealthPermissionCategory.RESTING_HEART_RATE, new ArrayList<>()));
720         expectedResponseMap.put(
721                 SkinTemperatureRecord.class,
722                 new RecordTypeInfoTestResponse(
723                         VITALS, HealthPermissionCategory.SKIN_TEMPERATURE, new ArrayList<>()));
724         expectedResponseMap.put(
725                 ActiveCaloriesBurnedRecord.class,
726                 new RecordTypeInfoTestResponse(
727                         ACTIVITY,
728                         HealthPermissionCategory.ACTIVE_CALORIES_BURNED,
729                         new ArrayList<>()));
730         expectedResponseMap.put(
731                 BodyFatRecord.class,
732                 new RecordTypeInfoTestResponse(
733                         BODY_MEASUREMENTS, HealthPermissionCategory.BODY_FAT, new ArrayList<>()));
734         expectedResponseMap.put(
735                 BodyTemperatureRecord.class,
736                 new RecordTypeInfoTestResponse(
737                         VITALS, HealthPermissionCategory.BODY_TEMPERATURE, new ArrayList<>()));
738         expectedResponseMap.put(
739                 NutritionRecord.class,
740                 new RecordTypeInfoTestResponse(
741                         NUTRITION, HealthPermissionCategory.NUTRITION, new ArrayList<>()));
742         expectedResponseMap.put(
743                 LeanBodyMassRecord.class,
744                 new RecordTypeInfoTestResponse(
745                         BODY_MEASUREMENTS,
746                         HealthPermissionCategory.LEAN_BODY_MASS,
747                         new ArrayList<>()));
748         expectedResponseMap.put(
749                 HeartRateVariabilityRmssdRecord.class,
750                 new RecordTypeInfoTestResponse(
751                         VITALS,
752                         HealthPermissionCategory.HEART_RATE_VARIABILITY,
753                         new ArrayList<>()));
754         expectedResponseMap.put(
755                 MenstruationFlowRecord.class,
756                 new RecordTypeInfoTestResponse(
757                         CYCLE_TRACKING, HealthPermissionCategory.MENSTRUATION, new ArrayList<>()));
758         expectedResponseMap.put(
759                 BloodGlucoseRecord.class,
760                 new RecordTypeInfoTestResponse(
761                         VITALS, HealthPermissionCategory.BLOOD_GLUCOSE, new ArrayList<>()));
762         expectedResponseMap.put(
763                 BloodPressureRecord.class,
764                 new RecordTypeInfoTestResponse(
765                         VITALS, HealthPermissionCategory.BLOOD_PRESSURE, new ArrayList<>()));
766         expectedResponseMap.put(
767                 CyclingPedalingCadenceRecord.class,
768                 new RecordTypeInfoTestResponse(ACTIVITY, EXERCISE, new ArrayList<>()));
769         expectedResponseMap.put(
770                 IntermenstrualBleedingRecord.class,
771                 new RecordTypeInfoTestResponse(
772                         CYCLE_TRACKING,
773                         HealthPermissionCategory.INTERMENSTRUAL_BLEEDING,
774                         new ArrayList<>()));
775         expectedResponseMap.put(
776                 FloorsClimbedRecord.class,
777                 new RecordTypeInfoTestResponse(
778                         ACTIVITY, HealthPermissionCategory.FLOORS_CLIMBED, new ArrayList<>()));
779         expectedResponseMap.put(
780                 StepsCadenceRecord.class,
781                 new RecordTypeInfoTestResponse(ACTIVITY, STEPS, new ArrayList<>()));
782         expectedResponseMap.put(
783                 HeightRecord.class,
784                 new RecordTypeInfoTestResponse(
785                         BODY_MEASUREMENTS, HealthPermissionCategory.HEIGHT, new ArrayList<>()));
786         expectedResponseMap.put(
787                 SexualActivityRecord.class,
788                 new RecordTypeInfoTestResponse(
789                         CYCLE_TRACKING,
790                         HealthPermissionCategory.SEXUAL_ACTIVITY,
791                         new ArrayList<>()));
792         expectedResponseMap.put(
793                 MenstruationPeriodRecord.class,
794                 new RecordTypeInfoTestResponse(
795                         CYCLE_TRACKING, HealthPermissionCategory.MENSTRUATION, new ArrayList<>()));
796         expectedResponseMap.put(
797                 SleepSessionRecord.class,
798                 new RecordTypeInfoTestResponse(
799                         SLEEP, HealthPermissionCategory.SLEEP, new ArrayList<>()));
800         expectedResponseMap.put(
801                 BasalMetabolicRateRecord.class,
802                 new RecordTypeInfoTestResponse(
803                         BODY_MEASUREMENTS, BASAL_METABOLIC_RATE, new ArrayList<>()));
804         expectedResponseMap.put(
805                 PlannedExerciseSessionRecord.class,
806                 new RecordTypeInfoTestResponse(ACTIVITY, PLANNED_EXERCISE, new ArrayList<>()));
807     }
808 
fetchDataOriginsPriorityOrder( int dataCategory)809     public static FetchDataOriginsPriorityOrderResponse fetchDataOriginsPriorityOrder(
810             int dataCategory) throws InterruptedException {
811         HealthConnectReceiver<FetchDataOriginsPriorityOrderResponse> receiver =
812                 new HealthConnectReceiver<>();
813         getHealthConnectManager()
814                 .fetchDataOriginsPriorityOrder(
815                         dataCategory, Executors.newSingleThreadExecutor(), receiver);
816         return receiver.getResponse();
817     }
818 
updateDataOriginPriorityOrder(UpdateDataOriginPriorityOrderRequest request)819     public static void updateDataOriginPriorityOrder(UpdateDataOriginPriorityOrderRequest request)
820             throws InterruptedException {
821         HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
822         getHealthConnectManager()
823                 .updateDataOriginPriorityOrder(
824                         request, Executors.newSingleThreadExecutor(), receiver);
825         receiver.verifyNoExceptionOrThrow();
826     }
827 
deleteTestData()828     public static void deleteTestData() throws InterruptedException {
829         verifyDeleteRecords(
830                 new DeleteUsingFiltersRequest.Builder()
831                         .setTimeRangeFilter(
832                                 new TimeInstantRangeFilter.Builder()
833                                         .setStartTime(Instant.EPOCH)
834                                         .setEndTime(Instant.now().plus(10, ChronoUnit.DAYS))
835                                         .build())
836                         .addRecordType(ExerciseSessionRecord.class)
837                         .addRecordType(StepsRecord.class)
838                         .addRecordType(HeartRateRecord.class)
839                         .addRecordType(BasalMetabolicRateRecord.class)
840                         .build());
841     }
842 
runShellCommand(String command)843     public static String runShellCommand(String command) throws IOException {
844         UiAutomation uiAutomation =
845                 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
846                         .getUiAutomation();
847         uiAutomation.adoptShellPermissionIdentity();
848         final ParcelFileDescriptor stdout = uiAutomation.executeShellCommand(command);
849         StringBuilder output = new StringBuilder();
850 
851         try (BufferedReader reader =
852                 new BufferedReader(
853                         new InputStreamReader(new FileInputStream(stdout.getFileDescriptor())))) {
854             char[] buffer = new char[4096];
855             int bytesRead;
856             while ((bytesRead = reader.read(buffer)) != -1) {
857                 output.append(buffer, 0, bytesRead);
858             }
859         } catch (FileNotFoundException e) {
860             Log.e(TAG, e.getMessage());
861         }
862 
863         return output.toString();
864     }
865 
866     @NonNull
getHealthConnectManager()867     static HealthConnectManager getHealthConnectManager() {
868         return getHealthConnectManager(ApplicationProvider.getApplicationContext());
869     }
870 
871     @NonNull
getHealthConnectManager(Context context)872     private static HealthConnectManager getHealthConnectManager(Context context) {
873         return requireNonNull(context.getSystemService(HealthConnectManager.class));
874     }
875 
getDeviceConfigValue(String key)876     public static String getDeviceConfigValue(String key) {
877         return runWithShellPermissionIdentity(
878                 () -> DeviceConfig.getProperty(DeviceConfig.NAMESPACE_HEALTH_FITNESS, key),
879                 READ_DEVICE_CONFIG);
880     }
881 
setDeviceConfigValue(String key, String value)882     public static void setDeviceConfigValue(String key, String value) {
883         runWithShellPermissionIdentity(
884                 () ->
885                         DeviceConfig.setProperty(
886                                 DeviceConfig.NAMESPACE_HEALTH_FITNESS, key, value, false),
887                 WRITE_ALLOWLISTED_DEVICE_CONFIG);
888     }
889 
890     /** Reads {@link StepsRecord}s using record IDs. */
readStepsRecordsUsingRecordIdsViaTestApp( Context context, List<String> recordIds)891     public static void readStepsRecordsUsingRecordIdsViaTestApp(
892             Context context, List<String> recordIds) {
893         Bundle extras = new Bundle();
894         extras.putStringArrayList(EXTRA_RECORD_IDS, new ArrayList<>(recordIds));
895         sendCommandToTestAppReceiver(context, ACTION_READ_STEPS_RECORDS_USING_RECORD_IDS, extras);
896     }
897 
898     /** Reads {@link StepsRecord}s using package name filters. */
readStepsRecordsUsingFiltersViaTestApp( Context context, List<String> packageNameFilters)899     public static void readStepsRecordsUsingFiltersViaTestApp(
900             Context context, List<String> packageNameFilters) {
901         Bundle extras = new Bundle();
902         extras.putStringArrayList(EXTRA_PACKAGE_NAMES, new ArrayList<>(packageNameFilters));
903         sendCommandToTestAppReceiver(context, ACTION_READ_STEPS_RECORDS_USING_FILTERS, extras);
904     }
905 
906     /** Aggregates {@link StepsRecord}s using package name filters. */
aggregateStepsCount(Context context, List<String> packageNameFilters)907     public static void aggregateStepsCount(Context context, List<String> packageNameFilters) {
908         Bundle extras = new Bundle();
909         extras.putStringArrayList(EXTRA_PACKAGE_NAMES, new ArrayList<>(packageNameFilters));
910         sendCommandToTestAppReceiver(context, ACTION_AGGREGATE_STEPS_COUNT, extras);
911     }
912 
sendCommandToTestAppReceiver(Context context, String action)913     public static void sendCommandToTestAppReceiver(Context context, String action) {
914         sendCommandToTestAppReceiver(context, action, /* extras= */ null);
915     }
916 
sendCommandToTestAppReceiver(Context context, String action, Bundle extras)917     public static void sendCommandToTestAppReceiver(Context context, String action, Bundle extras) {
918         // This call to reset() is important!
919         // reset() needs to be called every time before a call is made to the test app, otherwise,
920         // TestReceiver won't receive the result from the test app.
921         android.healthconnect.cts.utils.TestReceiver.reset();
922 
923         final Intent intent = new Intent(action).setClassName(PKG_TEST_APP, TEST_APP_RECEIVER);
924         intent.putExtra(EXTRA_SENDER_PACKAGE_NAME, context.getPackageName());
925         if (extras != null) {
926             intent.putExtras(extras);
927         }
928         context.sendBroadcast(intent);
929     }
930 
931     /** Sets up the priority list for aggregation tests. */
setupAggregation(String packageName, int dataCategory)932     public static void setupAggregation(String packageName, int dataCategory) {
933         try {
934             setupAggregation(
935                     record -> insertRecords(Collections.singletonList(record)),
936                     packageName,
937                     dataCategory);
938         } catch (Exception e) {
939             throw new RuntimeException(e);
940         }
941     }
942 
943     /** Sets up the priority list for aggregation tests. */
setupAggregation( ThrowingConsumer<Record> inserter, String packageName, int dataCategory)944     public static void setupAggregation(
945             ThrowingConsumer<Record> inserter, String packageName, int dataCategory)
946             throws Exception {
947         inserter.acceptOrThrow(getAnUnaggregatableRecord(packageName));
948         setupAggregation(List.of(packageName), dataCategory);
949     }
950 
951     /**
952      * Sets up the priority list for aggregation tests.
953      *
954      * <p>In order for this method to work, eac of the {@code packageNames} needs to have at least
955      * one record of any type in the HC DB before this method is called.
956      *
957      * <p>This is mainly used to setup priority list for a test app, so a test can read aggregation
958      * of data inserted by a test app. It would be nicer if this method take an instance of a test
959      * app such as {@code TestAppProxy}, however, it would requires this TestUtils class depends on
960      * the dependency where the TestAppProxy comes from, which then would create a dependency cycle
961      * because TestAppProxy's dependency is already using this TestUtils class.
962      */
setupAggregation(List<String> packageNames, int dataCategory)963     public static void setupAggregation(List<String> packageNames, int dataCategory)
964             throws Exception {
965         // Add the packageNames inserting the records to the priority list manually
966         // Since CTS tests get their permissions granted at install time and skip
967         // the Health Connect APIs that would otherwise add the packageName to the priority list
968         updatePriorityWithManageHealthDataPermission(dataCategory, packageNames);
969         FetchDataOriginsPriorityOrderResponse newPriority =
970                 getPriorityWithManageHealthDataPermission(dataCategory);
971         List<String> newPriorityString =
972                 newPriority.getDataOriginsPriorityOrder().stream()
973                         .map(DataOrigin::getPackageName)
974                         .toList();
975         assertThat(newPriorityString).isEqualTo(packageNames);
976     }
977 
978     /** Inserts a record that does not support aggregation to enable the priority list. */
insertRecordsForPriority(String packageName)979     public static void insertRecordsForPriority(String packageName) throws InterruptedException {
980         // Insert records that do not support aggregation so that the AppInfoTable is initialised
981         insertRecords(List.of(getAnUnaggregatableRecord(packageName)));
982     }
983 
984     /** Returns a {@link Record} that does not support aggregation. */
getAnUnaggregatableRecord(String packageName)985     private static Record getAnUnaggregatableRecord(String packageName) {
986         return new MenstruationPeriodRecord.Builder(
987                         new Metadata.Builder()
988                                 .setDataOrigin(
989                                         new DataOrigin.Builder()
990                                                 .setPackageName(packageName)
991                                                 .build())
992                                 .build(),
993                         NOW,
994                         NOW.plusMillis(1000))
995                 .build();
996     }
997 
998     /** Updates the priority list after getting the MANAGE_HEALTH_DATA permission. */
updatePriorityWithManageHealthDataPermission( int permissionCategory, List<String> packageNames)999     public static void updatePriorityWithManageHealthDataPermission(
1000             int permissionCategory, List<String> packageNames) throws InterruptedException {
1001         UiAutomation uiAutomation =
1002                 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
1003                         .getUiAutomation();
1004 
1005         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
1006         try {
1007             updatePriority(permissionCategory, packageNames);
1008         } finally {
1009             uiAutomation.dropShellPermissionIdentity();
1010         }
1011     }
1012 
1013     /** Updates the priority list without getting the MANAGE_HEALTH_DATA permission. */
updatePriority(int permissionCategory, List<String> packageNames)1014     public static void updatePriority(int permissionCategory, List<String> packageNames)
1015             throws InterruptedException {
1016         Context context = ApplicationProvider.getApplicationContext();
1017         HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
1018         assertThat(service).isNotNull();
1019 
1020         List<DataOrigin> dataOrigins =
1021                 packageNames.stream()
1022                         .map(
1023                                 (packageName) ->
1024                                         new DataOrigin.Builder()
1025                                                 .setPackageName(packageName)
1026                                                 .build())
1027                         .collect(Collectors.toList());
1028 
1029         CountDownLatch latch = new CountDownLatch(1);
1030         AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
1031                 new AtomicReference<>();
1032         UpdateDataOriginPriorityOrderRequest updateDataOriginPriorityOrderRequest =
1033                 new UpdateDataOriginPriorityOrderRequest(dataOrigins, permissionCategory);
1034         service.updateDataOriginPriorityOrder(
1035                 updateDataOriginPriorityOrderRequest,
1036                 Executors.newSingleThreadExecutor(),
1037                 new OutcomeReceiver<>() {
1038                     @Override
1039                     public void onResult(Void result) {
1040                         latch.countDown();
1041                     }
1042 
1043                     @Override
1044                     public void onError(HealthConnectException exception) {
1045                         healthConnectExceptionAtomicReference.set(exception);
1046                         latch.countDown();
1047                     }
1048                 });
1049         assertThat(updateDataOriginPriorityOrderRequest.getDataCategory())
1050                 .isEqualTo(permissionCategory);
1051         assertThat(updateDataOriginPriorityOrderRequest.getDataOriginInOrder()).isNotNull();
1052         assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
1053         if (healthConnectExceptionAtomicReference.get() != null) {
1054             throw healthConnectExceptionAtomicReference.get();
1055         }
1056     }
1057 
isHardwareSupported()1058     public static boolean isHardwareSupported() {
1059         return isHardwareSupported(ApplicationProvider.getApplicationContext());
1060     }
1061 
1062     /** returns true if the hardware is supported by HealthConnect. */
isHardwareSupported(Context context)1063     public static boolean isHardwareSupported(Context context) {
1064         PackageManager pm = context.getPackageManager();
1065         return (!pm.hasSystemFeature(PackageManager.FEATURE_EMBEDDED)
1066                 && !pm.hasSystemFeature(PackageManager.FEATURE_WATCH)
1067                 && !pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
1068                 && !pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE));
1069     }
1070 
1071     /** Gets the priority list after getting the MANAGE_HEALTH_DATA permission. */
getPriorityWithManageHealthDataPermission( int permissionCategory)1072     public static FetchDataOriginsPriorityOrderResponse getPriorityWithManageHealthDataPermission(
1073             int permissionCategory) throws InterruptedException {
1074         UiAutomation uiAutomation =
1075                 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
1076                         .getUiAutomation();
1077 
1078         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
1079         FetchDataOriginsPriorityOrderResponse response;
1080 
1081         try {
1082             response = getPriority(permissionCategory);
1083         } finally {
1084             uiAutomation.dropShellPermissionIdentity();
1085         }
1086 
1087         return response;
1088     }
1089 
1090     /** Gets the priority list without requesting the MANAGE_HEALTH_DATA permission. */
getPriority(int permissionCategory)1091     public static FetchDataOriginsPriorityOrderResponse getPriority(int permissionCategory)
1092             throws InterruptedException {
1093         Context context = ApplicationProvider.getApplicationContext();
1094         HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
1095         assertThat(service).isNotNull();
1096 
1097         AtomicReference<FetchDataOriginsPriorityOrderResponse> response = new AtomicReference<>();
1098         CountDownLatch latch = new CountDownLatch(1);
1099         AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
1100                 new AtomicReference<>();
1101         service.fetchDataOriginsPriorityOrder(
1102                 permissionCategory,
1103                 Executors.newSingleThreadExecutor(),
1104                 new OutcomeReceiver<>() {
1105                     @Override
1106                     public void onResult(FetchDataOriginsPriorityOrderResponse result) {
1107                         response.set(result);
1108                         latch.countDown();
1109                     }
1110 
1111                     @Override
1112                     public void onError(HealthConnectException exception) {
1113                         healthConnectExceptionAtomicReference.set(exception);
1114                         latch.countDown();
1115                     }
1116                 });
1117         assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
1118         if (healthConnectExceptionAtomicReference.get() != null) {
1119             throw healthConnectExceptionAtomicReference.get();
1120         }
1121 
1122         return response.get();
1123     }
1124 
1125     /**
1126      * Marks apps with any granted health permissions as connected to HC.
1127      *
1128      * <p>Test apps in CTS get their permissions auto granted without going through the HC
1129      * connection flow which prevents the HC service from recording the app info in the database.
1130      *
1131      * <p>This method calls "getCurrentPriority" API behind the scenes which has a side effect of
1132      * adding all the apps on the device with at least one health permission granted to the
1133      * database.
1134      */
connectAppsWithGrantedPermissions()1135     public static void connectAppsWithGrantedPermissions() {
1136         try {
1137             getPriorityWithManageHealthDataPermission(1);
1138         } catch (InterruptedException e) {
1139             throw new IllegalArgumentException(e);
1140         }
1141     }
1142 
1143     /** Zips given id and records lists to create a list of {@link RecordIdFilter}. */
getRecordIdFilters( List<String> recordIds, List<Record> records)1144     public static List<RecordIdFilter> getRecordIdFilters(
1145             List<String> recordIds, List<Record> records) {
1146         return IntStream.range(0, recordIds.size())
1147                 .mapToObj(
1148                         i -> {
1149                             Class<? extends Record> recordClass = records.get(i).getClass();
1150                             String id = recordIds.get(i);
1151                             return RecordIdFilter.fromId(recordClass, id);
1152                         })
1153                 .toList();
1154     }
1155 
1156     /** Creates an {@link Instant} representing the given local time yesterday at UTC. */
yesterdayAt(String localTime)1157     public static Instant yesterdayAt(String localTime) {
1158         return LocalTime.parse(localTime)
1159                 .atDate(LocalDate.now().minusDays(1))
1160                 .toInstant(ZoneOffset.UTC);
1161     }
1162 
1163     /** Inserts {@link StepsRecord} via test app with the specified data. */
insertStepsRecordViaTestApp( Context context, Instant startTime, Instant endTime, long value)1164     public static String insertStepsRecordViaTestApp(
1165             Context context, Instant startTime, Instant endTime, long value) {
1166         return insertStepsRecordViaTestApp(
1167                 context, startTime, endTime, /* clientId= */ null, value);
1168     }
1169 
1170     /** Inserts {@link StepsRecord} via test app with the specified data. */
insertStepsRecordViaTestApp( Context context, Instant startTime, Instant endTime, String clientId, long value)1171     public static String insertStepsRecordViaTestApp(
1172             Context context, Instant startTime, Instant endTime, String clientId, long value) {
1173         Bundle bundle = new Bundle();
1174         bundle.putLongArray(EXTRA_TIMES, new long[] {startTime.toEpochMilli()});
1175         bundle.putLongArray(EXTRA_END_TIMES, new long[] {endTime.toEpochMilli()});
1176         bundle.putStringArray(EXTRA_RECORD_CLIENT_IDS, new String[] {clientId});
1177         bundle.putLongArray(EXTRA_RECORD_VALUES, new long[] {value});
1178         sendCommandToTestAppReceiver(context, ACTION_INSERT_STEPS_RECORDS, bundle);
1179         return android.healthconnect.cts.utils.TestReceiver.getResult()
1180                 .getStringArrayList(EXTRA_RECORD_IDS)
1181                 .get(0);
1182     }
1183 
1184     /** Inserts {@link WeightRecord} via test app with the specified data. */
insertWeightRecordViaTestApp(Context context, Instant time, double value)1185     public static String insertWeightRecordViaTestApp(Context context, Instant time, double value) {
1186         return insertWeightRecordViaTestApp(context, time, /* clientId= */ null, value);
1187     }
1188 
1189     /** Inserts {@link WeightRecord} via test app with the specified data. */
insertWeightRecordViaTestApp( Context context, Instant time, String clientId, double value)1190     public static String insertWeightRecordViaTestApp(
1191             Context context, Instant time, String clientId, double value) {
1192         Bundle bundle = new Bundle();
1193         bundle.putLongArray(EXTRA_TIMES, new long[] {time.toEpochMilli()});
1194         bundle.putStringArray(EXTRA_RECORD_CLIENT_IDS, new String[] {clientId});
1195         bundle.putDoubleArray(EXTRA_RECORD_VALUES, new double[] {value});
1196         sendCommandToTestAppReceiver(context, ACTION_INSERT_WEIGHT_RECORDS, bundle);
1197         return android.healthconnect.cts.utils.TestReceiver.getResult()
1198                 .getStringArrayList(EXTRA_RECORD_IDS)
1199                 .get(0);
1200     }
1201 
1202     /** Inserts {@link StepsRecord} via test app with the specified data. */
insertExerciseRecordViaTestApp( Context context, Instant startTime, Instant endTime, String plannedExerciseSessionId)1203     public static String insertExerciseRecordViaTestApp(
1204             Context context, Instant startTime, Instant endTime, String plannedExerciseSessionId) {
1205         Bundle bundle = new Bundle();
1206         bundle.putLongArray(EXTRA_TIMES, new long[] {startTime.toEpochMilli()});
1207         bundle.putLongArray(EXTRA_END_TIMES, new long[] {endTime.toEpochMilli()});
1208         bundle.putString(EXTRA_PLANNED_EXERCISE_SESSION_ID, plannedExerciseSessionId);
1209         sendCommandToTestAppReceiver(context, ACTION_INSERT_EXERCISE_RECORD, bundle);
1210         return android.healthconnect.cts.utils.TestReceiver.getResult()
1211                 .getStringArrayList(EXTRA_RECORD_IDS)
1212                 .get(0);
1213     }
1214 
1215     /** Inserts {@link StepsRecord} via test app with the specified data. */
insertPlannedExerciseSessionRecordViaTestApp( Context context, Instant startTime, Instant endTime)1216     public static String insertPlannedExerciseSessionRecordViaTestApp(
1217             Context context, Instant startTime, Instant endTime) {
1218         Bundle bundle = new Bundle();
1219         bundle.putLongArray(EXTRA_TIMES, new long[] {startTime.toEpochMilli()});
1220         bundle.putLongArray(EXTRA_END_TIMES, new long[] {endTime.toEpochMilli()});
1221         sendCommandToTestAppReceiver(context, ACTION_INSERT_PLANNED_EXERCISE_RECORD, bundle);
1222         return android.healthconnect.cts.utils.TestReceiver.getResult()
1223                 .getStringArrayList(EXTRA_RECORD_IDS)
1224                 .get(0);
1225     }
1226 
1227     /** Extracts and returns ids of the provided records. */
getRecordIds(List<? extends Record> records)1228     public static List<String> getRecordIds(List<? extends Record> records) {
1229         return records.stream().map(Record::getMetadata).map(Metadata::getId).toList();
1230     }
1231 
1232     /**
1233      * Helper function to execute a request to create a medical data source and return the inserted
1234      * {@link MedicalDataSource} using {@link HealthConnectManager}.
1235      */
createMedicalDataSource(CreateMedicalDataSourceRequest request)1236     public static MedicalDataSource createMedicalDataSource(CreateMedicalDataSourceRequest request)
1237             throws InterruptedException {
1238         HealthConnectReceiver<MedicalDataSource> receiver = new HealthConnectReceiver<>();
1239         getHealthConnectManager()
1240                 .createMedicalDataSource(request, Executors.newSingleThreadExecutor(), receiver);
1241         return receiver.getResponse();
1242     }
1243 
1244     /** Helper function to read medical data sources from the DB, using HealthConnectManager. */
getMedicalDataSourcesByIds(List<String> ids)1245     public static List<MedicalDataSource> getMedicalDataSourcesByIds(List<String> ids)
1246             throws InterruptedException {
1247         HealthConnectReceiver<List<MedicalDataSource>> receiver = new HealthConnectReceiver<>();
1248         getHealthConnectManager()
1249                 .getMedicalDataSources(ids, Executors.newSingleThreadExecutor(), receiver);
1250         return receiver.getResponse();
1251     }
1252 
1253     /**
1254      * Helper function to read medical resources from the DB by a list of {@link MedicalIdFilter},
1255      * using HealthConnectManager.
1256      */
readMedicalResourcesByIds(List<MedicalIdFilter> ids)1257     public static List<MedicalResource> readMedicalResourcesByIds(List<MedicalIdFilter> ids)
1258             throws InterruptedException {
1259         HealthConnectReceiver<List<MedicalResource>> receiver = new HealthConnectReceiver<>();
1260         getHealthConnectManager()
1261                 .readMedicalResources(ids, Executors.newSingleThreadExecutor(), receiver);
1262         return receiver.getResponse();
1263     }
1264 
1265     /**
1266      * Helper function to read medical resources from the DB by a {@link
1267      * ReadMedicalResourcesResponse}, using HealthConnectManager.
1268      */
readMedicalResourcesByRequest( ReadMedicalResourcesRequest request)1269     public static ReadMedicalResourcesResponse readMedicalResourcesByRequest(
1270             ReadMedicalResourcesRequest request) throws InterruptedException {
1271         HealthConnectReceiver<ReadMedicalResourcesResponse> receiver =
1272                 new HealthConnectReceiver<>();
1273         getHealthConnectManager()
1274                 .readMedicalResources(request, Executors.newSingleThreadExecutor(), receiver);
1275         return receiver.getResponse();
1276     }
1277 
1278     /**
1279      * Creates a {@link ReadRecordsRequestUsingFilters} with the filters being a {@code clazz} and a
1280      * list of package names.
1281      */
1282     public static <T extends Record>
createReadRecordsRequestUsingFilters( Class<T> clazz, Collection<String> packageNameFilters)1283             ReadRecordsRequestUsingFilters<T> createReadRecordsRequestUsingFilters(
1284                     Class<T> clazz, Collection<String> packageNameFilters) {
1285         ReadRecordsRequestUsingFilters.Builder<T> builder =
1286                 new ReadRecordsRequestUsingFilters.Builder<>(clazz);
1287         for (String packageName : packageNameFilters) {
1288             builder.addDataOrigins(getDataOrigin(packageName));
1289         }
1290         return builder.build();
1291     }
1292 
1293     /** Copies record ids from the one list to another in order. Workaround for b/328228842. */
1294     // TODO(b/328228842): Avoid using reflection once we have Builder(Record) constructors
copyRecordIdsViaReflection( List<? extends Record> from, List<? extends Record> to)1295     public static void copyRecordIdsViaReflection(
1296             List<? extends Record> from, List<? extends Record> to) {
1297         assertThat(from).hasSize(to.size());
1298 
1299         for (int i = 0; i < from.size(); i++) {
1300             copyRecordIdViaReflection(from.get(i), to.get(i));
1301         }
1302     }
1303 
1304     // TODO(b/328228842): Avoid using reflection once we have Builder(Record) constructors
copyRecordIdViaReflection(Record from, Record to)1305     private static void copyRecordIdViaReflection(Record from, Record to) {
1306         setRecordIdViaReflection(to.getMetadata(), from.getMetadata().getId());
1307     }
1308 
1309     // TODO(b/328228842): Avoid using reflection once we have Builder(Record) constructors
setRecordIdViaReflection(Metadata metadata, String id)1310     private static void setRecordIdViaReflection(Metadata metadata, String id) {
1311         try {
1312             Field field = Metadata.class.getDeclaredField("mId");
1313             boolean isAccessible = field.isAccessible();
1314             field.setAccessible(true);
1315             field.set(metadata, id);
1316             field.setAccessible(isAccessible);
1317         } catch (Exception e) {
1318             throw new RuntimeException(e);
1319         }
1320     }
1321 
1322     public static final class RecordAndIdentifier {
1323         private final int mId;
1324         private final Record mRecordClass;
1325 
RecordAndIdentifier(int id, Record recordClass)1326         public RecordAndIdentifier(int id, Record recordClass) {
1327             this.mId = id;
1328             this.mRecordClass = recordClass;
1329         }
1330 
getId()1331         public int getId() {
1332             return mId;
1333         }
1334 
getRecordClass()1335         public Record getRecordClass() {
1336             return mRecordClass;
1337         }
1338     }
1339 
1340     public static class RecordTypeInfoTestResponse {
1341         private final int mRecordTypePermission;
1342         private final ArrayList<String> mContributingPackages;
1343         private final int mRecordTypeCategory;
1344 
RecordTypeInfoTestResponse( int recordTypeCategory, int recordTypePermission, ArrayList<String> contributingPackages)1345         RecordTypeInfoTestResponse(
1346                 int recordTypeCategory,
1347                 int recordTypePermission,
1348                 ArrayList<String> contributingPackages) {
1349             mRecordTypeCategory = recordTypeCategory;
1350             mRecordTypePermission = recordTypePermission;
1351             mContributingPackages = contributingPackages;
1352         }
1353 
getRecordTypeCategory()1354         public int getRecordTypeCategory() {
1355             return mRecordTypeCategory;
1356         }
1357 
getRecordTypePermission()1358         public int getRecordTypePermission() {
1359             return mRecordTypePermission;
1360         }
1361 
getContributingPackages()1362         public ArrayList<String> getContributingPackages() {
1363             return mContributingPackages;
1364         }
1365     }
1366 
1367     private static class TestReceiver<T, E extends RuntimeException>
1368             implements OutcomeReceiver<T, E> {
1369         private final CountDownLatch mLatch = new CountDownLatch(1);
1370         private final AtomicReference<T> mResponse = new AtomicReference<>();
1371         private final AtomicReference<E> mException = new AtomicReference<>();
1372 
getResponse()1373         public T getResponse() throws InterruptedException {
1374             verifyNoExceptionOrThrow();
1375             return mResponse.get();
1376         }
1377 
verifyNoExceptionOrThrow()1378         public void verifyNoExceptionOrThrow() throws InterruptedException {
1379             assertThat(mLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
1380             if (mException.get() != null) {
1381                 throw mException.get();
1382             }
1383         }
1384 
1385         @Override
onResult(T result)1386         public void onResult(T result) {
1387             mResponse.set(result);
1388             mLatch.countDown();
1389         }
1390 
1391         @Override
onError(@onNull E error)1392         public void onError(@NonNull E error) {
1393             mException.set(error);
1394             Log.e(TAG, error.getMessage());
1395             mLatch.countDown();
1396         }
1397     }
1398 
1399     private static final class HealthConnectReceiver<T>
1400             extends TestReceiver<T, HealthConnectException> {}
1401 
1402     public static final class MigrationReceiver extends TestReceiver<Void, MigrationException> {}
1403 
1404     /**
1405      * A {@link Consumer} that allows throwing checked exceptions from its single abstract method.
1406      */
1407     @FunctionalInterface
1408     @SuppressWarnings("FunctionalInterfaceMethodChanged")
1409     public interface ThrowingConsumer<T> extends Consumer<T> {
1410         /** Implementations of this method might throw exception. */
acceptOrThrow(T t)1411         void acceptOrThrow(T t) throws Exception;
1412 
1413         @Override
accept(T t)1414         default void accept(T t) {
1415             try {
1416                 acceptOrThrow(t);
1417             } catch (Exception ex) {
1418                 throw new RuntimeException(ex);
1419             }
1420         }
1421     }
1422 }
1423