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