• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.healthconnect.cts.utils;
18 
19 import static android.health.connect.HealthDataCategory.ACTIVITY;
20 import static android.health.connect.HealthDataCategory.BODY_MEASUREMENTS;
21 import static android.health.connect.HealthDataCategory.CYCLE_TRACKING;
22 import static android.health.connect.HealthDataCategory.NUTRITION;
23 import static android.health.connect.HealthDataCategory.SLEEP;
24 import static android.health.connect.HealthDataCategory.VITALS;
25 import static android.health.connect.HealthPermissionCategory.BASAL_METABOLIC_RATE;
26 import static android.health.connect.HealthPermissionCategory.EXERCISE;
27 import static android.health.connect.HealthPermissionCategory.HEART_RATE;
28 import static android.health.connect.HealthPermissionCategory.PLANNED_EXERCISE;
29 import static android.health.connect.HealthPermissionCategory.STEPS;
30 import static android.health.connect.HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION;
31 import static android.healthconnect.cts.utils.DataFactory.NOW;
32 import static android.healthconnect.cts.utils.DataFactory.getDataOrigin;
33 import static android.healthconnect.cts.utils.HealthConnectReceiver.callAndGetResponseWithShellPermissionIdentity;
34 
35 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
36 import static com.android.healthfitness.flags.AconfigFlagHelper.isPersonalHealthRecordEnabled;
37 
38 import static com.google.common.truth.Truth.assertThat;
39 
40 import static java.util.Collections.unmodifiableList;
41 import static java.util.Objects.requireNonNull;
42 
43 import android.Manifest;
44 import android.app.UiAutomation;
45 import android.content.Context;
46 import android.content.pm.PackageManager;
47 import android.health.connect.AggregateRecordsGroupedByDurationResponse;
48 import android.health.connect.AggregateRecordsGroupedByPeriodResponse;
49 import android.health.connect.AggregateRecordsRequest;
50 import android.health.connect.AggregateRecordsResponse;
51 import android.health.connect.ApplicationInfoResponse;
52 import android.health.connect.DeleteUsingFiltersRequest;
53 import android.health.connect.FetchDataOriginsPriorityOrderResponse;
54 import android.health.connect.GetMedicalDataSourcesRequest;
55 import android.health.connect.HealthConnectException;
56 import android.health.connect.HealthConnectManager;
57 import android.health.connect.HealthPermissionCategory;
58 import android.health.connect.InsertRecordsResponse;
59 import android.health.connect.ReadRecordsRequest;
60 import android.health.connect.ReadRecordsRequestUsingFilters;
61 import android.health.connect.ReadRecordsRequestUsingIds;
62 import android.health.connect.ReadRecordsResponse;
63 import android.health.connect.RecordIdFilter;
64 import android.health.connect.RecordTypeInfoResponse;
65 import android.health.connect.TimeInstantRangeFilter;
66 import android.health.connect.UpdateDataOriginPriorityOrderRequest;
67 import android.health.connect.accesslog.AccessLog;
68 import android.health.connect.changelog.ChangeLogTokenRequest;
69 import android.health.connect.changelog.ChangeLogTokenResponse;
70 import android.health.connect.changelog.ChangeLogsRequest;
71 import android.health.connect.changelog.ChangeLogsResponse;
72 import android.health.connect.datatypes.ActiveCaloriesBurnedRecord;
73 import android.health.connect.datatypes.AppInfo;
74 import android.health.connect.datatypes.BasalBodyTemperatureRecord;
75 import android.health.connect.datatypes.BasalMetabolicRateRecord;
76 import android.health.connect.datatypes.BloodGlucoseRecord;
77 import android.health.connect.datatypes.BloodPressureRecord;
78 import android.health.connect.datatypes.BodyFatRecord;
79 import android.health.connect.datatypes.BodyTemperatureRecord;
80 import android.health.connect.datatypes.BodyWaterMassRecord;
81 import android.health.connect.datatypes.BoneMassRecord;
82 import android.health.connect.datatypes.CervicalMucusRecord;
83 import android.health.connect.datatypes.CyclingPedalingCadenceRecord;
84 import android.health.connect.datatypes.DataOrigin;
85 import android.health.connect.datatypes.DistanceRecord;
86 import android.health.connect.datatypes.ElevationGainedRecord;
87 import android.health.connect.datatypes.ExerciseSessionRecord;
88 import android.health.connect.datatypes.FloorsClimbedRecord;
89 import android.health.connect.datatypes.HeartRateRecord;
90 import android.health.connect.datatypes.HeartRateVariabilityRmssdRecord;
91 import android.health.connect.datatypes.HeightRecord;
92 import android.health.connect.datatypes.HydrationRecord;
93 import android.health.connect.datatypes.IntermenstrualBleedingRecord;
94 import android.health.connect.datatypes.LeanBodyMassRecord;
95 import android.health.connect.datatypes.MedicalDataSource;
96 import android.health.connect.datatypes.MenstruationFlowRecord;
97 import android.health.connect.datatypes.MenstruationPeriodRecord;
98 import android.health.connect.datatypes.Metadata;
99 import android.health.connect.datatypes.NutritionRecord;
100 import android.health.connect.datatypes.OvulationTestRecord;
101 import android.health.connect.datatypes.OxygenSaturationRecord;
102 import android.health.connect.datatypes.PlannedExerciseSessionRecord;
103 import android.health.connect.datatypes.PowerRecord;
104 import android.health.connect.datatypes.Record;
105 import android.health.connect.datatypes.RespiratoryRateRecord;
106 import android.health.connect.datatypes.RestingHeartRateRecord;
107 import android.health.connect.datatypes.SexualActivityRecord;
108 import android.health.connect.datatypes.SkinTemperatureRecord;
109 import android.health.connect.datatypes.SleepSessionRecord;
110 import android.health.connect.datatypes.SpeedRecord;
111 import android.health.connect.datatypes.StepsCadenceRecord;
112 import android.health.connect.datatypes.StepsRecord;
113 import android.health.connect.datatypes.TotalCaloriesBurnedRecord;
114 import android.health.connect.datatypes.Vo2MaxRecord;
115 import android.health.connect.datatypes.WeightRecord;
116 import android.health.connect.datatypes.WheelchairPushesRecord;
117 import android.health.connect.migration.MigrationException;
118 import android.os.OutcomeReceiver;
119 import android.os.ParcelFileDescriptor;
120 import android.util.Log;
121 
122 import androidx.annotation.NonNull;
123 import androidx.test.core.app.ApplicationProvider;
124 import androidx.test.platform.app.InstrumentationRegistry;
125 
126 import com.android.healthfitness.flags.Flags;
127 
128 import java.io.BufferedReader;
129 import java.io.FileInputStream;
130 import java.io.FileNotFoundException;
131 import java.io.IOException;
132 import java.io.InputStreamReader;
133 import java.lang.reflect.Field;
134 import java.time.Duration;
135 import java.time.Instant;
136 import java.time.LocalDate;
137 import java.time.LocalTime;
138 import java.time.Period;
139 import java.time.ZoneOffset;
140 import java.time.temporal.ChronoUnit;
141 import java.util.ArrayList;
142 import java.util.Arrays;
143 import java.util.Collection;
144 import java.util.Collections;
145 import java.util.HashMap;
146 import java.util.List;
147 import java.util.Map;
148 import java.util.Set;
149 import java.util.concurrent.ConcurrentHashMap;
150 import java.util.concurrent.CountDownLatch;
151 import java.util.concurrent.ExecutorService;
152 import java.util.concurrent.Executors;
153 import java.util.concurrent.TimeUnit;
154 import java.util.concurrent.atomic.AtomicReference;
155 import java.util.function.Consumer;
156 import java.util.function.Predicate;
157 import java.util.stream.Collectors;
158 import java.util.stream.IntStream;
159 
160 public final class TestUtils {
161     private static final String TAG = "HCTestUtils";
162 
getChangeLogToken(ChangeLogTokenRequest request)163     public static ChangeLogTokenResponse getChangeLogToken(ChangeLogTokenRequest request)
164             throws InterruptedException {
165         return getChangeLogToken(request, ApplicationProvider.getApplicationContext());
166     }
167 
getChangeLogToken( ChangeLogTokenRequest request, Context context)168     public static ChangeLogTokenResponse getChangeLogToken(
169             ChangeLogTokenRequest request, Context context) throws InterruptedException {
170         HealthConnectReceiver<ChangeLogTokenResponse> receiver = new HealthConnectReceiver<>();
171         getHealthConnectManager(context)
172                 .getChangeLogToken(request, Executors.newSingleThreadExecutor(), receiver);
173         return receiver.getResponse();
174     }
175 
insertRecordAndGetId(Record record)176     public static String insertRecordAndGetId(Record record) throws InterruptedException {
177         return insertRecords(Collections.singletonList(record)).get(0).getMetadata().getId();
178     }
179 
insertRecordAndGetId(Record record, Context context)180     public static String insertRecordAndGetId(Record record, Context context)
181             throws InterruptedException {
182         return insertRecords(Collections.singletonList(record), context)
183                 .get(0)
184                 .getMetadata()
185                 .getId();
186     }
187 
188     /**
189      * Insert record to the database.
190      *
191      * @param record record to insert
192      * @return inserted record
193      */
insertRecord(Record record)194     public static Record insertRecord(Record record) throws InterruptedException {
195         return insertRecords(Collections.singletonList(record)).get(0);
196     }
197 
198     /**
199      * Inserts records to the database.
200      *
201      * @param records records to insert
202      * @return inserted records
203      */
insertRecords(List<? extends Record> records)204     public static List<Record> insertRecords(List<? extends Record> records)
205             throws InterruptedException {
206         return insertRecords(records, ApplicationProvider.getApplicationContext());
207     }
208 
209     /**
210      * Inserts records to the database.
211      *
212      * @param records records to insert
213      * @return inserted records
214      */
insertRecords(Record... records)215     public static List<Record> insertRecords(Record... records) throws InterruptedException {
216         return insertRecords(Arrays.asList(records), ApplicationProvider.getApplicationContext());
217     }
218 
219     /**
220      * Inserts records to the database.
221      *
222      * @param records records to insert.
223      * @param context a {@link Context} to obtain {@link HealthConnectManager}.
224      * @return inserted records.
225      */
insertRecords(List<? extends Record> records, Context context)226     public static List<Record> insertRecords(List<? extends Record> records, Context context)
227             throws InterruptedException {
228         HealthConnectReceiver<InsertRecordsResponse> receiver = new HealthConnectReceiver<>();
229         getHealthConnectManager(context)
230                 .insertRecords(
231                         unmodifiableList(records), Executors.newSingleThreadExecutor(), receiver);
232         List<Record> returnedRecords = receiver.getResponse().getRecords();
233         assertThat(returnedRecords).hasSize(records.size());
234         return returnedRecords;
235     }
236 
237     /**
238      * Returns all records from the `records` list in their original order, but distinct by UUID.
239      */
distinctByUuid(List<T> records)240     public static <T extends Record> List<T> distinctByUuid(List<T> records) {
241         return records.stream().filter(distinctByUuid()).toList();
242     }
243 
distinctByUuid()244     private static Predicate<? super Record> distinctByUuid() {
245         Set<String> seen = ConcurrentHashMap.newKeySet();
246         return record -> seen.add(record.getMetadata().getId());
247     }
248 
249     /** Updates the provided records in the database. */
updateRecords(List<? extends Record> records)250     public static void updateRecords(List<? extends Record> records) throws InterruptedException {
251         updateRecords(records, ApplicationProvider.getApplicationContext());
252     }
253 
254     /** Synchronously updates records in HC. */
updateRecords(List<? extends Record> records, Context context)255     public static void updateRecords(List<? extends Record> records, Context context)
256             throws InterruptedException {
257         HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
258         getHealthConnectManager(context)
259                 .updateRecords(
260                         unmodifiableList(records), Executors.newSingleThreadExecutor(), receiver);
261         receiver.verifyNoExceptionOrThrow();
262     }
263 
getChangeLogs(ChangeLogsRequest changeLogsRequest)264     public static ChangeLogsResponse getChangeLogs(ChangeLogsRequest changeLogsRequest)
265             throws InterruptedException {
266         return getChangeLogs(changeLogsRequest, ApplicationProvider.getApplicationContext());
267     }
268 
getChangeLogs( ChangeLogsRequest changeLogsRequest, Context context)269     public static ChangeLogsResponse getChangeLogs(
270             ChangeLogsRequest changeLogsRequest, Context context) throws InterruptedException {
271         HealthConnectReceiver<ChangeLogsResponse> receiver = new HealthConnectReceiver<>();
272         getHealthConnectManager(context)
273                 .getChangeLogs(changeLogsRequest, Executors.newSingleThreadExecutor(), receiver);
274         return receiver.getResponse();
275     }
276 
277     /**
278      * Given {@link android.health.connect.HealthPermissions#MANAGE_HEALTH_DATA_PERMISSION}, invokes
279      * {@link HealthConnectManager#aggregate} with the given {@code request}.
280      */
getAggregateResponseWithManagePermission( AggregateRecordsRequest<T> request)281     public static <T> AggregateRecordsResponse<T> getAggregateResponseWithManagePermission(
282             AggregateRecordsRequest<T> request) throws InterruptedException {
283         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
284         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA_PERMISSION);
285 
286         try {
287             return getAggregateResponse(request);
288         } finally {
289             uiAutomation.dropShellPermissionIdentity();
290         }
291     }
292 
getAggregateResponse( AggregateRecordsRequest<T> request)293     public static <T> AggregateRecordsResponse<T> getAggregateResponse(
294             AggregateRecordsRequest<T> request) throws InterruptedException {
295         HealthConnectReceiver<AggregateRecordsResponse<T>> receiver =
296                 new HealthConnectReceiver<AggregateRecordsResponse<T>>();
297         getHealthConnectManager().aggregate(request, Executors.newSingleThreadExecutor(), receiver);
298         return receiver.getResponse();
299     }
300 
getAggregateResponse( AggregateRecordsRequest<T> request, List<Record> recordsToInsert)301     public static <T> AggregateRecordsResponse<T> getAggregateResponse(
302             AggregateRecordsRequest<T> request, List<Record> recordsToInsert)
303             throws InterruptedException {
304         if (recordsToInsert != null) {
305             insertRecords(recordsToInsert);
306         }
307 
308         HealthConnectReceiver<AggregateRecordsResponse<T>> receiver = new HealthConnectReceiver<>();
309         getHealthConnectManager().aggregate(request, Executors.newSingleThreadExecutor(), receiver);
310         return receiver.getResponse();
311     }
312 
313     public static <T>
getAggregateResponseGroupByDuration( AggregateRecordsRequest<T> request, Duration duration)314             List<AggregateRecordsGroupedByDurationResponse<T>> getAggregateResponseGroupByDuration(
315                     AggregateRecordsRequest<T> request, Duration duration)
316                     throws InterruptedException {
317         HealthConnectReceiver<List<AggregateRecordsGroupedByDurationResponse<T>>> receiver =
318                 new HealthConnectReceiver<>();
319         getHealthConnectManager()
320                 .aggregateGroupByDuration(
321                         request, duration, Executors.newSingleThreadExecutor(), receiver);
322         return receiver.getResponse();
323     }
324 
325     public static <T>
getAggregateResponseGroupByPeriod( AggregateRecordsRequest<T> request, Period period)326             List<AggregateRecordsGroupedByPeriodResponse<T>> getAggregateResponseGroupByPeriod(
327                     AggregateRecordsRequest<T> request, Period period) throws InterruptedException {
328         HealthConnectReceiver<List<AggregateRecordsGroupedByPeriodResponse<T>>> receiver =
329                 new HealthConnectReceiver<>();
330         getHealthConnectManager()
331                 .aggregateGroupByPeriod(
332                         request, period, Executors.newSingleThreadExecutor(), receiver);
333         return receiver.getResponse();
334     }
335 
336     /**
337      * Given {@link android.health.connect.HealthPermissions#MANAGE_HEALTH_DATA_PERMISSION}, invokes
338      * {@link HealthConnectManager#readRecords} with the given {@code request}.
339      */
readRecordsWithManagePermission( ReadRecordsRequest<T> request)340     public static <T extends Record> List<T> readRecordsWithManagePermission(
341             ReadRecordsRequest<T> request) throws InterruptedException {
342         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
343         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA_PERMISSION);
344 
345         try {
346             return readRecords(request);
347         } finally {
348             uiAutomation.dropShellPermissionIdentity();
349         }
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_PERMISSION);
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_PERMISSION);
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 
454     /**
455      * Delete all health records (datasources, resources etc) stored in the Health Connect database.
456      */
deleteAllMedicalData()457     public static void deleteAllMedicalData() throws InterruptedException {
458         if (!isPersonalHealthRecordEnabled()) {
459             return;
460         }
461         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
462         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA_PERMISSION);
463         try {
464             HealthConnectReceiver<List<MedicalDataSource>> receiver = new HealthConnectReceiver<>();
465             HealthConnectManager manager = getHealthConnectManager();
466             ExecutorService executor = Executors.newSingleThreadExecutor();
467             manager.getMedicalDataSources(
468                     new GetMedicalDataSourcesRequest.Builder().build(), executor, receiver);
469             List<MedicalDataSource> dataSources = receiver.getResponse();
470             for (MedicalDataSource dataSource : dataSources) {
471                 HealthConnectReceiver<Void> callback = new HealthConnectReceiver<>();
472                 manager.deleteMedicalDataSourceWithData(dataSource.getId(), executor, callback);
473                 callback.verifyNoExceptionOrThrow();
474             }
475         } finally {
476             uiAutomation.dropShellPermissionIdentity();
477         }
478     }
479 
verifyDeleteRecords(List<RecordIdFilter> request)480     public static void verifyDeleteRecords(List<RecordIdFilter> request)
481             throws InterruptedException {
482         verifyDeleteRecords(request, ApplicationProvider.getApplicationContext());
483     }
484 
verifyDeleteRecords(List<RecordIdFilter> request, Context context)485     public static void verifyDeleteRecords(List<RecordIdFilter> request, Context context)
486             throws InterruptedException {
487         HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
488         getHealthConnectManager(context)
489                 .deleteRecords(request, Executors.newSingleThreadExecutor(), receiver);
490         receiver.verifyNoExceptionOrThrow();
491     }
492 
verifyDeleteRecords( Class<? extends Record> recordType, TimeInstantRangeFilter timeRangeFilter)493     public static void verifyDeleteRecords(
494             Class<? extends Record> recordType, TimeInstantRangeFilter timeRangeFilter)
495             throws InterruptedException {
496         HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
497         getHealthConnectManager()
498                 .deleteRecords(
499                         recordType, timeRangeFilter, Executors.newSingleThreadExecutor(), receiver);
500         receiver.verifyNoExceptionOrThrow();
501     }
502 
503     /** Helper function to delete records from the DB using HealthConnectManager. */
deleteRecords(List<? extends Record> records)504     public static void deleteRecords(List<? extends Record> records) throws InterruptedException {
505         List<RecordIdFilter> recordIdFilters =
506                 records.stream()
507                         .map(
508                                 (record ->
509                                         RecordIdFilter.fromId(
510                                                 record.getClass(), record.getMetadata().getId())))
511                         .collect(Collectors.toList());
512         verifyDeleteRecords(recordIdFilters);
513     }
514 
515     /** Helper function to delete records from the DB, using HealthConnectManager. */
deleteRecordsByIdFilter(List<RecordIdFilter> recordIdFilters)516     public static void deleteRecordsByIdFilter(List<RecordIdFilter> recordIdFilters)
517             throws InterruptedException {
518         verifyDeleteRecords(recordIdFilters);
519     }
520 
queryAccessLogs()521     public static List<AccessLog> queryAccessLogs() throws InterruptedException {
522         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
523         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA_PERMISSION);
524         try {
525             HealthConnectReceiver<List<AccessLog>> receiver = new HealthConnectReceiver<>();
526             getHealthConnectManager()
527                     .queryAccessLogs(Executors.newSingleThreadExecutor(), receiver);
528             return receiver.getResponse();
529         } finally {
530             uiAutomation.dropShellPermissionIdentity();
531         }
532     }
533 
queryAllRecordTypesInfo()534     public static Map<Class<? extends Record>, RecordTypeInfoResponse> queryAllRecordTypesInfo()
535             throws InterruptedException, NullPointerException {
536         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
537         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA_PERMISSION);
538         try {
539             HealthConnectReceiver<Map<Class<? extends Record>, RecordTypeInfoResponse>> receiver =
540                     new HealthConnectReceiver<>();
541             getHealthConnectManager()
542                     .queryAllRecordTypesInfo(Executors.newSingleThreadExecutor(), receiver);
543             return receiver.getResponse();
544         } finally {
545             uiAutomation.dropShellPermissionIdentity();
546         }
547     }
548 
getActivityDates(List<Class<? extends Record>> recordTypes)549     public static List<LocalDate> getActivityDates(List<Class<? extends Record>> recordTypes)
550             throws InterruptedException {
551         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
552         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA_PERMISSION);
553         try {
554             HealthConnectReceiver<List<LocalDate>> receiver = new HealthConnectReceiver<>();
555             getHealthConnectManager()
556                     .queryActivityDates(recordTypes, Executors.newSingleThreadExecutor(), receiver);
557             return receiver.getResponse();
558         } finally {
559             uiAutomation.dropShellPermissionIdentity();
560         }
561     }
562 
563     /** Calls {@link HealthConnectManager#startMigration} ()} with shell permission identity. */
startMigrationWithShellPermissionIdentity()564     public static void startMigrationWithShellPermissionIdentity() throws InterruptedException {
565         callAndGetResponseWithShellPermissionIdentity(
566                 MigrationException.class,
567                 getHealthConnectManager()::startMigration,
568                 Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA);
569     }
570 
571     /** Calls {@link HealthConnectManager#finishMigration} with shell permission identity. */
finishMigrationWithShellPermissionIdentity()572     public static void finishMigrationWithShellPermissionIdentity() throws InterruptedException {
573         callAndGetResponseWithShellPermissionIdentity(
574                 MigrationException.class,
575                 getHealthConnectManager()::finishMigration,
576                 Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA);
577     }
578 
579     /** Calls insertMinDataMigrationSdkExtensionVersion with shell permission identity. */
insertMinDataMigrationSdkExtensionVersionWithShellPermissionIdentity( int version)580     public static void insertMinDataMigrationSdkExtensionVersionWithShellPermissionIdentity(
581             int version) throws InterruptedException {
582         Void unused =
583                 callAndGetResponseWithShellPermissionIdentity(
584                         MigrationException.class,
585                         (executor, receiver) ->
586                                 getHealthConnectManager()
587                                         .insertMinDataMigrationSdkExtensionVersion(
588                                                 version, executor, receiver),
589                         Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA);
590     }
591 
deleteAllStagedRemoteData()592     public static void deleteAllStagedRemoteData() {
593         deleteAllStagedRemoteData(getHealthConnectManager());
594     }
595 
deleteAllStagedRemoteData(HealthConnectManager service)596     public static void deleteAllStagedRemoteData(HealthConnectManager service) {
597         runWithShellPermissionIdentity(
598                 () ->
599                         // TODO(b/241542162): Avoid reflection once TestApi can be called from CTS
600                         service.getClass().getMethod("deleteAllStagedRemoteData").invoke(service),
601                 "android.permission.DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA");
602     }
603 
604     /** Set lower rate limits for testing */
setLowerRateLimitsForTesting(boolean enabled)605     public static boolean setLowerRateLimitsForTesting(boolean enabled) {
606         HealthConnectManager service = getHealthConnectManager();
607         try {
608             runWithShellPermissionIdentity(
609                     () ->
610                             // TODO(b/241542162): Avoid reflection once TestApi can be called from
611                             // CTS
612                             service.getClass()
613                                     .getMethod("setLowerRateLimitsForTesting", boolean.class)
614                                     .invoke(service, enabled),
615                     "android.permission.DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA");
616             return true;
617         } catch (RuntimeException e) {
618             // Old versions of the module don't have this API.
619             Log.e(TAG, "Couldn't override quota for testing", e);
620             return false;
621         }
622     }
623 
getHealthConnectDataMigrationState()624     public static int getHealthConnectDataMigrationState() throws InterruptedException {
625         return callAndGetResponseWithShellPermissionIdentity(
626                         getHealthConnectManager()::getHealthConnectDataState,
627                         MANAGE_HEALTH_DATA_PERMISSION)
628                 .getDataMigrationState();
629     }
630 
getHealthConnectDataRestoreState()631     public static int getHealthConnectDataRestoreState() throws InterruptedException {
632         return callAndGetResponseWithShellPermissionIdentity(
633                         getHealthConnectManager()::getHealthConnectDataState,
634                         MANAGE_HEALTH_DATA_PERMISSION)
635                 .getDataRestoreState();
636     }
637 
getApplicationInfo()638     public static List<AppInfo> getApplicationInfo() throws InterruptedException {
639         HealthConnectReceiver<ApplicationInfoResponse> receiver = new HealthConnectReceiver<>();
640         getHealthConnectManager()
641                 .getContributorApplicationsInfo(Executors.newSingleThreadExecutor(), receiver);
642         return receiver.getResponse().getApplicationInfoList();
643     }
644 
getRecordById(List<T> list, String id)645     public static <T extends Record> T getRecordById(List<T> list, String id) {
646         for (T record : list) {
647             if (record.getMetadata().getId().equals(id)) {
648                 return record;
649             }
650         }
651 
652         throw new AssertionError("Record not found with id: " + id);
653     }
654 
populateAndResetExpectedResponseMap( HashMap<Class<? extends Record>, RecordTypeInfoTestResponse> expectedResponseMap)655     public static void populateAndResetExpectedResponseMap(
656             HashMap<Class<? extends Record>, RecordTypeInfoTestResponse> expectedResponseMap) {
657         expectedResponseMap.put(
658                 ElevationGainedRecord.class,
659                 new RecordTypeInfoTestResponse(
660                         ACTIVITY, HealthPermissionCategory.ELEVATION_GAINED, new ArrayList<>()));
661         expectedResponseMap.put(
662                 OvulationTestRecord.class,
663                 new RecordTypeInfoTestResponse(
664                         CYCLE_TRACKING,
665                         HealthPermissionCategory.OVULATION_TEST,
666                         new ArrayList<>()));
667         expectedResponseMap.put(
668                 DistanceRecord.class,
669                 new RecordTypeInfoTestResponse(
670                         ACTIVITY, HealthPermissionCategory.DISTANCE, new ArrayList<>()));
671         expectedResponseMap.put(
672                 SpeedRecord.class,
673                 new RecordTypeInfoTestResponse(
674                         ACTIVITY, HealthPermissionCategory.SPEED, new ArrayList<>()));
675 
676         expectedResponseMap.put(
677                 Vo2MaxRecord.class,
678                 new RecordTypeInfoTestResponse(
679                         ACTIVITY, HealthPermissionCategory.VO2_MAX, new ArrayList<>()));
680         expectedResponseMap.put(
681                 OxygenSaturationRecord.class,
682                 new RecordTypeInfoTestResponse(
683                         VITALS, HealthPermissionCategory.OXYGEN_SATURATION, new ArrayList<>()));
684         expectedResponseMap.put(
685                 TotalCaloriesBurnedRecord.class,
686                 new RecordTypeInfoTestResponse(
687                         ACTIVITY,
688                         HealthPermissionCategory.TOTAL_CALORIES_BURNED,
689                         new ArrayList<>()));
690         expectedResponseMap.put(
691                 HydrationRecord.class,
692                 new RecordTypeInfoTestResponse(
693                         NUTRITION, HealthPermissionCategory.HYDRATION, new ArrayList<>()));
694         expectedResponseMap.put(
695                 StepsRecord.class,
696                 new RecordTypeInfoTestResponse(ACTIVITY, STEPS, new ArrayList<>()));
697         expectedResponseMap.put(
698                 CervicalMucusRecord.class,
699                 new RecordTypeInfoTestResponse(
700                         CYCLE_TRACKING,
701                         HealthPermissionCategory.CERVICAL_MUCUS,
702                         new ArrayList<>()));
703         expectedResponseMap.put(
704                 ExerciseSessionRecord.class,
705                 new RecordTypeInfoTestResponse(ACTIVITY, EXERCISE, new ArrayList<>()));
706         expectedResponseMap.put(
707                 HeartRateRecord.class,
708                 new RecordTypeInfoTestResponse(VITALS, HEART_RATE, new ArrayList<>()));
709         expectedResponseMap.put(
710                 RespiratoryRateRecord.class,
711                 new RecordTypeInfoTestResponse(
712                         VITALS, HealthPermissionCategory.RESPIRATORY_RATE, new ArrayList<>()));
713         expectedResponseMap.put(
714                 BasalBodyTemperatureRecord.class,
715                 new RecordTypeInfoTestResponse(
716                         VITALS,
717                         HealthPermissionCategory.BASAL_BODY_TEMPERATURE,
718                         new ArrayList<>()));
719         expectedResponseMap.put(
720                 WheelchairPushesRecord.class,
721                 new RecordTypeInfoTestResponse(
722                         ACTIVITY, HealthPermissionCategory.WHEELCHAIR_PUSHES, new ArrayList<>()));
723         expectedResponseMap.put(
724                 PowerRecord.class,
725                 new RecordTypeInfoTestResponse(
726                         ACTIVITY, HealthPermissionCategory.POWER, new ArrayList<>()));
727         expectedResponseMap.put(
728                 BodyWaterMassRecord.class,
729                 new RecordTypeInfoTestResponse(
730                         BODY_MEASUREMENTS,
731                         HealthPermissionCategory.BODY_WATER_MASS,
732                         new ArrayList<>()));
733         expectedResponseMap.put(
734                 WeightRecord.class,
735                 new RecordTypeInfoTestResponse(
736                         BODY_MEASUREMENTS, HealthPermissionCategory.WEIGHT, new ArrayList<>()));
737         expectedResponseMap.put(
738                 BoneMassRecord.class,
739                 new RecordTypeInfoTestResponse(
740                         BODY_MEASUREMENTS, HealthPermissionCategory.BONE_MASS, new ArrayList<>()));
741         expectedResponseMap.put(
742                 RestingHeartRateRecord.class,
743                 new RecordTypeInfoTestResponse(
744                         VITALS, HealthPermissionCategory.RESTING_HEART_RATE, new ArrayList<>()));
745         expectedResponseMap.put(
746                 SkinTemperatureRecord.class,
747                 new RecordTypeInfoTestResponse(
748                         VITALS, HealthPermissionCategory.SKIN_TEMPERATURE, new ArrayList<>()));
749         expectedResponseMap.put(
750                 ActiveCaloriesBurnedRecord.class,
751                 new RecordTypeInfoTestResponse(
752                         ACTIVITY,
753                         HealthPermissionCategory.ACTIVE_CALORIES_BURNED,
754                         new ArrayList<>()));
755         expectedResponseMap.put(
756                 BodyFatRecord.class,
757                 new RecordTypeInfoTestResponse(
758                         BODY_MEASUREMENTS, HealthPermissionCategory.BODY_FAT, new ArrayList<>()));
759         expectedResponseMap.put(
760                 BodyTemperatureRecord.class,
761                 new RecordTypeInfoTestResponse(
762                         VITALS, HealthPermissionCategory.BODY_TEMPERATURE, new ArrayList<>()));
763         expectedResponseMap.put(
764                 NutritionRecord.class,
765                 new RecordTypeInfoTestResponse(
766                         NUTRITION, HealthPermissionCategory.NUTRITION, new ArrayList<>()));
767         expectedResponseMap.put(
768                 LeanBodyMassRecord.class,
769                 new RecordTypeInfoTestResponse(
770                         BODY_MEASUREMENTS,
771                         HealthPermissionCategory.LEAN_BODY_MASS,
772                         new ArrayList<>()));
773         expectedResponseMap.put(
774                 HeartRateVariabilityRmssdRecord.class,
775                 new RecordTypeInfoTestResponse(
776                         VITALS,
777                         HealthPermissionCategory.HEART_RATE_VARIABILITY,
778                         new ArrayList<>()));
779         expectedResponseMap.put(
780                 MenstruationFlowRecord.class,
781                 new RecordTypeInfoTestResponse(
782                         CYCLE_TRACKING, HealthPermissionCategory.MENSTRUATION, new ArrayList<>()));
783         expectedResponseMap.put(
784                 BloodGlucoseRecord.class,
785                 new RecordTypeInfoTestResponse(
786                         VITALS, HealthPermissionCategory.BLOOD_GLUCOSE, new ArrayList<>()));
787         expectedResponseMap.put(
788                 BloodPressureRecord.class,
789                 new RecordTypeInfoTestResponse(
790                         VITALS, HealthPermissionCategory.BLOOD_PRESSURE, new ArrayList<>()));
791         expectedResponseMap.put(
792                 CyclingPedalingCadenceRecord.class,
793                 new RecordTypeInfoTestResponse(ACTIVITY, EXERCISE, new ArrayList<>()));
794         expectedResponseMap.put(
795                 IntermenstrualBleedingRecord.class,
796                 new RecordTypeInfoTestResponse(
797                         CYCLE_TRACKING,
798                         HealthPermissionCategory.INTERMENSTRUAL_BLEEDING,
799                         new ArrayList<>()));
800         expectedResponseMap.put(
801                 FloorsClimbedRecord.class,
802                 new RecordTypeInfoTestResponse(
803                         ACTIVITY, HealthPermissionCategory.FLOORS_CLIMBED, new ArrayList<>()));
804         expectedResponseMap.put(
805                 StepsCadenceRecord.class,
806                 new RecordTypeInfoTestResponse(ACTIVITY, STEPS, new ArrayList<>()));
807         expectedResponseMap.put(
808                 HeightRecord.class,
809                 new RecordTypeInfoTestResponse(
810                         BODY_MEASUREMENTS, HealthPermissionCategory.HEIGHT, new ArrayList<>()));
811         expectedResponseMap.put(
812                 SexualActivityRecord.class,
813                 new RecordTypeInfoTestResponse(
814                         CYCLE_TRACKING,
815                         HealthPermissionCategory.SEXUAL_ACTIVITY,
816                         new ArrayList<>()));
817         expectedResponseMap.put(
818                 MenstruationPeriodRecord.class,
819                 new RecordTypeInfoTestResponse(
820                         CYCLE_TRACKING, HealthPermissionCategory.MENSTRUATION, new ArrayList<>()));
821         expectedResponseMap.put(
822                 SleepSessionRecord.class,
823                 new RecordTypeInfoTestResponse(
824                         SLEEP, HealthPermissionCategory.SLEEP, new ArrayList<>()));
825         expectedResponseMap.put(
826                 BasalMetabolicRateRecord.class,
827                 new RecordTypeInfoTestResponse(
828                         BODY_MEASUREMENTS, BASAL_METABOLIC_RATE, new ArrayList<>()));
829         expectedResponseMap.put(
830                 PlannedExerciseSessionRecord.class,
831                 new RecordTypeInfoTestResponse(ACTIVITY, PLANNED_EXERCISE, new ArrayList<>()));
832     }
833 
fetchDataOriginsPriorityOrder( int dataCategory)834     public static FetchDataOriginsPriorityOrderResponse fetchDataOriginsPriorityOrder(
835             int dataCategory) throws InterruptedException {
836         HealthConnectReceiver<FetchDataOriginsPriorityOrderResponse> receiver =
837                 new HealthConnectReceiver<>();
838         getHealthConnectManager()
839                 .fetchDataOriginsPriorityOrder(
840                         dataCategory, Executors.newSingleThreadExecutor(), receiver);
841         return receiver.getResponse();
842     }
843 
updateDataOriginPriorityOrder(UpdateDataOriginPriorityOrderRequest request)844     public static void updateDataOriginPriorityOrder(UpdateDataOriginPriorityOrderRequest request)
845             throws InterruptedException {
846         HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
847         getHealthConnectManager()
848                 .updateDataOriginPriorityOrder(
849                         request, Executors.newSingleThreadExecutor(), receiver);
850         receiver.verifyNoExceptionOrThrow();
851     }
852 
deleteTestData()853     public static void deleteTestData() throws InterruptedException {
854         verifyDeleteRecords(
855                 new DeleteUsingFiltersRequest.Builder()
856                         .setTimeRangeFilter(
857                                 new TimeInstantRangeFilter.Builder()
858                                         .setStartTime(Instant.EPOCH)
859                                         .setEndTime(Instant.now().plus(10, ChronoUnit.DAYS))
860                                         .build())
861                         .addRecordType(ExerciseSessionRecord.class)
862                         .addRecordType(StepsRecord.class)
863                         .addRecordType(HeartRateRecord.class)
864                         .addRecordType(BasalMetabolicRateRecord.class)
865                         .build());
866     }
867 
runShellCommand(String command)868     public static String runShellCommand(String command) throws IOException {
869         UiAutomation uiAutomation =
870                 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
871                         .getUiAutomation();
872         uiAutomation.adoptShellPermissionIdentity();
873         final ParcelFileDescriptor stdout = uiAutomation.executeShellCommand(command);
874         StringBuilder output = new StringBuilder();
875 
876         try (BufferedReader reader =
877                 new BufferedReader(
878                         new InputStreamReader(new FileInputStream(stdout.getFileDescriptor())))) {
879             char[] buffer = new char[4096];
880             int bytesRead;
881             while ((bytesRead = reader.read(buffer)) != -1) {
882                 output.append(buffer, 0, bytesRead);
883             }
884         } catch (FileNotFoundException e) {
885             Log.e(TAG, e.getMessage());
886         } finally {
887             uiAutomation.dropShellPermissionIdentity();
888         }
889 
890         return output.toString();
891     }
892 
893     @NonNull
getHealthConnectManager()894     public static HealthConnectManager getHealthConnectManager() {
895         return getHealthConnectManager(ApplicationProvider.getApplicationContext());
896     }
897 
898     @NonNull
getHealthConnectManager(Context context)899     public static HealthConnectManager getHealthConnectManager(Context context) {
900         return requireNonNull(context.getSystemService(HealthConnectManager.class));
901     }
902 
903     /** Sets up the priority list for aggregation tests. */
setupAggregation(String packageName, int dataCategory)904     public static void setupAggregation(String packageName, int dataCategory) {
905         try {
906             setupAggregation(
907                     record -> insertRecords(Collections.singletonList(record)),
908                     packageName,
909                     dataCategory);
910         } catch (Exception e) {
911             throw new RuntimeException(e);
912         }
913     }
914 
915     /** Sets up the priority list for aggregation tests. */
setupAggregation( ThrowingConsumer<Record> inserter, String packageName, int dataCategory)916     public static void setupAggregation(
917             ThrowingConsumer<Record> inserter, String packageName, int dataCategory)
918             throws Exception {
919         inserter.acceptOrThrow(getAnUnaggregatableRecord(packageName));
920         setupAggregation(List.of(packageName), dataCategory);
921     }
922 
923     /**
924      * Sets up the priority list for aggregation tests.
925      *
926      * <p>In order for this method to work, eac of the {@code packageNames} needs to have at least
927      * one record of any type in the HC DB before this method is called.
928      *
929      * <p>This is mainly used to setup priority list for a test app, so a test can read aggregation
930      * of data inserted by a test app. It would be nicer if this method take an instance of a test
931      * app such as {@code TestAppProxy}, however, it would requires this TestUtils class depends on
932      * the dependency where the TestAppProxy comes from, which then would create a dependency cycle
933      * because TestAppProxy's dependency is already using this TestUtils class.
934      */
setupAggregation(List<String> packageNames, int dataCategory)935     public static void setupAggregation(List<String> packageNames, int dataCategory)
936             throws Exception {
937         // Add the packageNames inserting the records to the priority list manually
938         // Since CTS tests get their permissions granted at install time and skip
939         // the Health Connect APIs that would otherwise add the packageName to the priority list
940         updatePriorityWithManageHealthDataPermission(dataCategory, packageNames);
941         FetchDataOriginsPriorityOrderResponse newPriority =
942                 getPriorityWithManageHealthDataPermission(dataCategory);
943         List<String> newPriorityString =
944                 newPriority.getDataOriginsPriorityOrder().stream()
945                         .map(DataOrigin::getPackageName)
946                         .toList();
947         assertThat(newPriorityString).isEqualTo(packageNames);
948     }
949 
950     /** Inserts a record that does not support aggregation to enable the priority list. */
insertRecordsForPriority(String packageName)951     public static void insertRecordsForPriority(String packageName) throws InterruptedException {
952         // Insert records that do not support aggregation so that the AppInfoTable is initialised
953         insertRecords(List.of(getAnUnaggregatableRecord(packageName)));
954     }
955 
956     /** Returns a {@link Record} that does not support aggregation. */
getAnUnaggregatableRecord(String packageName)957     private static Record getAnUnaggregatableRecord(String packageName) {
958         return new MenstruationPeriodRecord.Builder(
959                         new Metadata.Builder()
960                                 .setDataOrigin(
961                                         new DataOrigin.Builder()
962                                                 .setPackageName(packageName)
963                                                 .build())
964                                 .build(),
965                         NOW,
966                         NOW.plusMillis(1000))
967                 .build();
968     }
969 
970     /** Updates the priority list after getting the MANAGE_HEALTH_DATA permission. */
updatePriorityWithManageHealthDataPermission( int permissionCategory, List<String> packageNames)971     public static void updatePriorityWithManageHealthDataPermission(
972             int permissionCategory, List<String> packageNames) throws InterruptedException {
973         UiAutomation uiAutomation =
974                 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
975                         .getUiAutomation();
976 
977         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA_PERMISSION);
978         try {
979             updatePriority(permissionCategory, packageNames);
980         } finally {
981             uiAutomation.dropShellPermissionIdentity();
982         }
983     }
984 
985     /** Updates the priority list without getting the MANAGE_HEALTH_DATA permission. */
updatePriority(int permissionCategory, List<String> packageNames)986     public static void updatePriority(int permissionCategory, List<String> packageNames)
987             throws InterruptedException {
988         Context context = ApplicationProvider.getApplicationContext();
989         HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
990         assertThat(service).isNotNull();
991 
992         List<DataOrigin> dataOrigins =
993                 packageNames.stream()
994                         .map(
995                                 (packageName) ->
996                                         new DataOrigin.Builder()
997                                                 .setPackageName(packageName)
998                                                 .build())
999                         .collect(Collectors.toList());
1000 
1001         HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
1002         UpdateDataOriginPriorityOrderRequest updateDataOriginPriorityOrderRequest =
1003                 new UpdateDataOriginPriorityOrderRequest(dataOrigins, permissionCategory);
1004         service.updateDataOriginPriorityOrder(
1005                 updateDataOriginPriorityOrderRequest,
1006                 Executors.newSingleThreadExecutor(),
1007                 receiver);
1008 
1009         assertThat(updateDataOriginPriorityOrderRequest.getDataCategory())
1010                 .isEqualTo(permissionCategory);
1011         assertThat(updateDataOriginPriorityOrderRequest.getDataOriginInOrder()).isNotNull();
1012         receiver.verifyNoExceptionOrThrow(3);
1013     }
1014 
areHealthPermissionsSupported()1015     public static boolean areHealthPermissionsSupported() {
1016         return areHealthPermissionsSupported(ApplicationProvider.getApplicationContext());
1017     }
1018 
1019     /** returns true if the hardware is supported by HealthConnect. */
areHealthPermissionsSupported(Context context)1020     public static boolean areHealthPermissionsSupported(Context context) {
1021         PackageManager pm = context.getPackageManager();
1022         boolean isWatchEnabled =
1023                 pm.hasSystemFeature(PackageManager.FEATURE_WATCH)
1024                         && Flags.replaceBodySensorPermissionEnabled();
1025         return DeviceSupportUtils.isHealthConnectFullySupported(context) || isWatchEnabled;
1026     }
1027 
1028     /** Gets the priority list after getting the MANAGE_HEALTH_DATA permission. */
getPriorityWithManageHealthDataPermission( int permissionCategory)1029     public static FetchDataOriginsPriorityOrderResponse getPriorityWithManageHealthDataPermission(
1030             int permissionCategory) throws InterruptedException {
1031         UiAutomation uiAutomation =
1032                 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
1033                         .getUiAutomation();
1034 
1035         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA_PERMISSION);
1036         FetchDataOriginsPriorityOrderResponse response;
1037 
1038         try {
1039             response = getPriority(permissionCategory);
1040         } finally {
1041             uiAutomation.dropShellPermissionIdentity();
1042         }
1043 
1044         return response;
1045     }
1046 
1047     /** Gets the priority list without requesting the MANAGE_HEALTH_DATA permission. */
getPriority(int permissionCategory)1048     public static FetchDataOriginsPriorityOrderResponse getPriority(int permissionCategory)
1049             throws InterruptedException {
1050         Context context = ApplicationProvider.getApplicationContext();
1051         HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
1052         assertThat(service).isNotNull();
1053 
1054         AtomicReference<FetchDataOriginsPriorityOrderResponse> response = new AtomicReference<>();
1055         CountDownLatch latch = new CountDownLatch(1);
1056         AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
1057                 new AtomicReference<>();
1058         service.fetchDataOriginsPriorityOrder(
1059                 permissionCategory,
1060                 Executors.newSingleThreadExecutor(),
1061                 new OutcomeReceiver<>() {
1062                     @Override
1063                     public void onResult(FetchDataOriginsPriorityOrderResponse result) {
1064                         response.set(result);
1065                         latch.countDown();
1066                     }
1067 
1068                     @Override
1069                     public void onError(HealthConnectException exception) {
1070                         healthConnectExceptionAtomicReference.set(exception);
1071                         latch.countDown();
1072                     }
1073                 });
1074         assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
1075         if (healthConnectExceptionAtomicReference.get() != null) {
1076             throw healthConnectExceptionAtomicReference.get();
1077         }
1078 
1079         return response.get();
1080     }
1081 
1082     /**
1083      * Marks apps with any granted health permissions as connected to HC.
1084      *
1085      * <p>Test apps in CTS get their permissions auto granted without going through the HC
1086      * connection flow which prevents the HC service from recording the app info in the database.
1087      *
1088      * <p>This method calls "getCurrentPriority" API behind the scenes which has a side effect of
1089      * adding all the apps on the device with at least one health permission granted to the
1090      * database.
1091      */
connectAppsWithGrantedPermissions()1092     public static void connectAppsWithGrantedPermissions() {
1093         try {
1094             getPriorityWithManageHealthDataPermission(1);
1095         } catch (InterruptedException e) {
1096             throw new IllegalArgumentException(e);
1097         }
1098     }
1099 
1100     /** Zips given id and records lists to create a list of {@link RecordIdFilter}. */
getRecordIdFilters( List<String> recordIds, List<Record> records)1101     public static List<RecordIdFilter> getRecordIdFilters(
1102             List<String> recordIds, List<Record> records) {
1103         return IntStream.range(0, recordIds.size())
1104                 .mapToObj(
1105                         i -> {
1106                             Class<? extends Record> recordClass = records.get(i).getClass();
1107                             String id = recordIds.get(i);
1108                             return RecordIdFilter.fromId(recordClass, id);
1109                         })
1110                 .toList();
1111     }
1112 
1113     /** Creates an {@link Instant} representing the given local time yesterday at UTC. */
yesterdayAt(String localTime)1114     public static Instant yesterdayAt(String localTime) {
1115         return LocalTime.parse(localTime)
1116                 .atDate(LocalDate.now().minusDays(1))
1117                 .toInstant(ZoneOffset.UTC);
1118     }
1119 
1120     /** Extracts and returns ids of the provided records. */
getRecordIds(List<? extends Record> records)1121     public static List<String> getRecordIds(List<? extends Record> records) {
1122         return records.stream().map(Record::getMetadata).map(Metadata::getId).toList();
1123     }
1124 
1125     /**
1126      * Creates a {@link ReadRecordsRequestUsingFilters} with the filters being a {@code clazz} and a
1127      * list of package names.
1128      */
1129     public static <T extends Record>
createReadRecordsRequestUsingFilters( Class<T> clazz, Collection<String> packageNameFilters)1130             ReadRecordsRequestUsingFilters<T> createReadRecordsRequestUsingFilters(
1131                     Class<T> clazz, Collection<String> packageNameFilters) {
1132         ReadRecordsRequestUsingFilters.Builder<T> builder =
1133                 new ReadRecordsRequestUsingFilters.Builder<>(clazz);
1134         for (String packageName : packageNameFilters) {
1135             builder.addDataOrigins(getDataOrigin(packageName));
1136         }
1137         return builder.build();
1138     }
1139 
1140     /** Copies record ids from the one list to another in order. Workaround for b/328228842. */
1141     // TODO(b/328228842): Avoid using reflection once we have Builder(Record) constructors
copyRecordIdsViaReflection( List<? extends Record> from, List<? extends Record> to)1142     public static void copyRecordIdsViaReflection(
1143             List<? extends Record> from, List<? extends Record> to) {
1144         assertThat(from).hasSize(to.size());
1145 
1146         for (int i = 0; i < from.size(); i++) {
1147             copyRecordIdViaReflection(from.get(i), to.get(i));
1148         }
1149     }
1150 
1151     /**
1152      * Sets value for a field using reflection. This can be used to set fields for immutable class.
1153      *
1154      * <p>This method recursively looks for the field in the object's class and its superclasses.
1155      */
setFieldValueUsingReflection(Object object, String fieldName, Object value)1156     public static void setFieldValueUsingReflection(Object object, String fieldName, Object value)
1157             throws NoSuchFieldException, IllegalAccessException {
1158         Field field = findFieldUsingReflection(object.getClass(), fieldName);
1159         field.setAccessible(true);
1160         field.set(object, value);
1161     }
1162 
findFieldUsingReflection(Class<?> type, String fieldName)1163     private static Field findFieldUsingReflection(Class<?> type, String fieldName) {
1164         try {
1165             return type.getDeclaredField(fieldName);
1166         } catch (NoSuchFieldException e) {
1167             // If field isn't present, recursively look for it in the class's superclass.
1168             Class<?> superClass = type.getSuperclass();
1169             if (superClass != null) {
1170                 return findFieldUsingReflection(superClass, fieldName);
1171             }
1172         }
1173         throw new IllegalArgumentException("Could not find field " + fieldName);
1174     }
1175 
1176     // TODO(b/328228842): Avoid using reflection once we have Builder(Record) constructors
copyRecordIdViaReflection(Record from, Record to)1177     private static void copyRecordIdViaReflection(Record from, Record to) {
1178         setRecordIdViaReflection(to.getMetadata(), from.getMetadata().getId());
1179     }
1180 
1181     // TODO(b/328228842): Avoid using reflection once we have Builder(Record) constructors
setRecordIdViaReflection(Metadata metadata, String id)1182     private static void setRecordIdViaReflection(Metadata metadata, String id) {
1183         try {
1184             Field field = Metadata.class.getDeclaredField("mId");
1185             boolean isAccessible = field.isAccessible();
1186             field.setAccessible(true);
1187             field.set(metadata, id);
1188             field.setAccessible(isAccessible);
1189         } catch (Exception e) {
1190             throw new RuntimeException(e);
1191         }
1192     }
1193 
1194     public static class RecordTypeInfoTestResponse {
1195         private final int mRecordTypePermission;
1196         private final ArrayList<String> mContributingPackages;
1197         private final int mRecordTypeCategory;
1198 
RecordTypeInfoTestResponse( int recordTypeCategory, int recordTypePermission, ArrayList<String> contributingPackages)1199         RecordTypeInfoTestResponse(
1200                 int recordTypeCategory,
1201                 int recordTypePermission,
1202                 ArrayList<String> contributingPackages) {
1203             mRecordTypeCategory = recordTypeCategory;
1204             mRecordTypePermission = recordTypePermission;
1205             mContributingPackages = contributingPackages;
1206         }
1207 
getRecordTypeCategory()1208         public int getRecordTypeCategory() {
1209             return mRecordTypeCategory;
1210         }
1211 
getRecordTypePermission()1212         public int getRecordTypePermission() {
1213             return mRecordTypePermission;
1214         }
1215 
getContributingPackages()1216         public ArrayList<String> getContributingPackages() {
1217             return mContributingPackages;
1218         }
1219     }
1220 
1221     /**
1222      * A {@link Consumer} that allows throwing checked exceptions from its single abstract method.
1223      */
1224     @FunctionalInterface
1225     @SuppressWarnings("FunctionalInterfaceMethodChanged")
1226     public interface ThrowingConsumer<T> extends Consumer<T> {
1227         /** Implementations of this method might throw exception. */
acceptOrThrow(T t)1228         void acceptOrThrow(T t) throws Exception;
1229 
1230         @Override
accept(T t)1231         default void accept(T t) {
1232             try {
1233                 acceptOrThrow(t);
1234             } catch (Exception ex) {
1235                 throw new RuntimeException(ex);
1236             }
1237         }
1238     }
1239 }
1240