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