| /* |
| * Copyright (C) 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.healthconnect.cts.utils; |
| |
| import static android.content.pm.PackageManager.GET_PERMISSIONS; |
| import static android.health.connect.HealthDataCategory.ACTIVITY; |
| import static android.health.connect.HealthDataCategory.BODY_MEASUREMENTS; |
| import static android.health.connect.HealthDataCategory.CYCLE_TRACKING; |
| import static android.health.connect.HealthDataCategory.NUTRITION; |
| import static android.health.connect.HealthDataCategory.SLEEP; |
| import static android.health.connect.HealthDataCategory.VITALS; |
| import static android.health.connect.HealthPermissionCategory.BASAL_METABOLIC_RATE; |
| import static android.health.connect.HealthPermissionCategory.EXERCISE; |
| import static android.health.connect.HealthPermissionCategory.HEART_RATE; |
| import static android.health.connect.HealthPermissionCategory.STEPS; |
| import static android.health.connect.datatypes.Metadata.RECORDING_METHOD_ACTIVELY_RECORDED; |
| import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_BASAL_METABOLIC_RATE; |
| import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_HEART_RATE; |
| import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_STEPS; |
| import static android.healthconnect.test.app.TestAppReceiver.EXTRA_SENDER_PACKAGE_NAME; |
| |
| import static com.android.compatibility.common.util.FeatureUtil.AUTOMOTIVE_FEATURE; |
| import static com.android.compatibility.common.util.FeatureUtil.hasSystemFeature; |
| import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static java.util.Collections.unmodifiableList; |
| import static java.util.Objects.requireNonNull; |
| |
| import android.app.UiAutomation; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.health.connect.AggregateRecordsGroupedByDurationResponse; |
| import android.health.connect.AggregateRecordsGroupedByPeriodResponse; |
| import android.health.connect.AggregateRecordsRequest; |
| import android.health.connect.AggregateRecordsResponse; |
| import android.health.connect.ApplicationInfoResponse; |
| import android.health.connect.DeleteUsingFiltersRequest; |
| import android.health.connect.FetchDataOriginsPriorityOrderResponse; |
| import android.health.connect.HealthConnectDataState; |
| import android.health.connect.HealthConnectException; |
| import android.health.connect.HealthConnectManager; |
| import android.health.connect.HealthPermissionCategory; |
| import android.health.connect.HealthPermissions; |
| import android.health.connect.InsertRecordsResponse; |
| import android.health.connect.ReadRecordsRequest; |
| import android.health.connect.ReadRecordsRequestUsingFilters; |
| import android.health.connect.ReadRecordsRequestUsingIds; |
| import android.health.connect.ReadRecordsResponse; |
| import android.health.connect.RecordIdFilter; |
| import android.health.connect.RecordTypeInfoResponse; |
| import android.health.connect.TimeInstantRangeFilter; |
| import android.health.connect.UpdateDataOriginPriorityOrderRequest; |
| import android.health.connect.accesslog.AccessLog; |
| import android.health.connect.changelog.ChangeLogTokenRequest; |
| import android.health.connect.changelog.ChangeLogTokenResponse; |
| import android.health.connect.changelog.ChangeLogsRequest; |
| import android.health.connect.changelog.ChangeLogsResponse; |
| import android.health.connect.datatypes.ActiveCaloriesBurnedRecord; |
| import android.health.connect.datatypes.AppInfo; |
| import android.health.connect.datatypes.BasalBodyTemperatureRecord; |
| import android.health.connect.datatypes.BasalMetabolicRateRecord; |
| import android.health.connect.datatypes.BloodGlucoseRecord; |
| import android.health.connect.datatypes.BloodPressureRecord; |
| import android.health.connect.datatypes.BodyFatRecord; |
| import android.health.connect.datatypes.BodyTemperatureRecord; |
| import android.health.connect.datatypes.BodyWaterMassRecord; |
| import android.health.connect.datatypes.BoneMassRecord; |
| import android.health.connect.datatypes.CervicalMucusRecord; |
| import android.health.connect.datatypes.CyclingPedalingCadenceRecord; |
| import android.health.connect.datatypes.DataOrigin; |
| import android.health.connect.datatypes.Device; |
| import android.health.connect.datatypes.DistanceRecord; |
| import android.health.connect.datatypes.ElevationGainedRecord; |
| import android.health.connect.datatypes.ExerciseLap; |
| import android.health.connect.datatypes.ExerciseRoute; |
| import android.health.connect.datatypes.ExerciseSegment; |
| import android.health.connect.datatypes.ExerciseSegmentType; |
| import android.health.connect.datatypes.ExerciseSessionRecord; |
| import android.health.connect.datatypes.ExerciseSessionType; |
| import android.health.connect.datatypes.FloorsClimbedRecord; |
| import android.health.connect.datatypes.HeartRateRecord; |
| import android.health.connect.datatypes.HeartRateVariabilityRmssdRecord; |
| import android.health.connect.datatypes.HeightRecord; |
| import android.health.connect.datatypes.HydrationRecord; |
| import android.health.connect.datatypes.IntermenstrualBleedingRecord; |
| import android.health.connect.datatypes.LeanBodyMassRecord; |
| import android.health.connect.datatypes.MenstruationFlowRecord; |
| import android.health.connect.datatypes.MenstruationPeriodRecord; |
| import android.health.connect.datatypes.Metadata; |
| import android.health.connect.datatypes.NutritionRecord; |
| import android.health.connect.datatypes.OvulationTestRecord; |
| import android.health.connect.datatypes.OxygenSaturationRecord; |
| import android.health.connect.datatypes.PowerRecord; |
| import android.health.connect.datatypes.Record; |
| import android.health.connect.datatypes.RespiratoryRateRecord; |
| import android.health.connect.datatypes.RestingHeartRateRecord; |
| import android.health.connect.datatypes.SexualActivityRecord; |
| import android.health.connect.datatypes.SleepSessionRecord; |
| import android.health.connect.datatypes.SpeedRecord; |
| import android.health.connect.datatypes.StepsCadenceRecord; |
| import android.health.connect.datatypes.StepsRecord; |
| import android.health.connect.datatypes.TotalCaloriesBurnedRecord; |
| import android.health.connect.datatypes.Vo2MaxRecord; |
| import android.health.connect.datatypes.WeightRecord; |
| import android.health.connect.datatypes.WheelchairPushesRecord; |
| import android.health.connect.datatypes.units.Length; |
| import android.health.connect.datatypes.units.Power; |
| import android.health.connect.migration.MigrationEntity; |
| import android.health.connect.migration.MigrationException; |
| import android.healthconnect.test.app.TestAppReceiver; |
| import android.os.Bundle; |
| import android.os.OutcomeReceiver; |
| import android.os.ParcelFileDescriptor; |
| import android.os.UserHandle; |
| import android.util.Log; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.test.core.app.ApplicationProvider; |
| import androidx.test.platform.app.InstrumentationRegistry; |
| |
| import com.android.compatibility.common.util.SystemUtil; |
| import com.android.cts.install.lib.TestApp; |
| |
| import java.io.BufferedReader; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.Serializable; |
| import java.time.Duration; |
| import java.time.Instant; |
| import java.time.LocalDate; |
| import java.time.LocalDateTime; |
| import java.time.Period; |
| import java.time.ZoneId; |
| import java.time.ZoneOffset; |
| import java.time.format.DateTimeFormatter; |
| import java.time.temporal.ChronoUnit; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.UUID; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.stream.Collectors; |
| |
| public final class TestUtils { |
| public static final String MANAGE_HEALTH_PERMISSIONS = |
| HealthPermissions.MANAGE_HEALTH_PERMISSIONS; |
| public static final String READ_EXERCISE_ROUTE_PERMISSION = |
| "android.permission.health.READ_EXERCISE_ROUTE"; |
| private static final String HEALTH_PERMISSION_PREFIX = "android.permission.health."; |
| public static final String MANAGE_HEALTH_DATA = HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION; |
| public static final Instant SESSION_START_TIME = Instant.now().minus(10, ChronoUnit.DAYS); |
| public static final Instant SESSION_END_TIME = |
| Instant.now().minus(10, ChronoUnit.DAYS).plus(1, ChronoUnit.HOURS); |
| private static final String TAG = "HCTestUtils"; |
| private static final int TIMEOUT_SECONDS = 5; |
| |
| private static final String PKG_TEST_APP = "android.healthconnect.test.app"; |
| private static final String TEST_APP_RECEIVER = |
| PKG_TEST_APP + "." + TestAppReceiver.class.getSimpleName(); |
| |
| public static boolean isHardwareAutomotive() { |
| return hasSystemFeature(AUTOMOTIVE_FEATURE); |
| } |
| |
| public static ChangeLogTokenResponse getChangeLogToken(ChangeLogTokenRequest request) |
| throws InterruptedException { |
| return getChangeLogToken(request, ApplicationProvider.getApplicationContext()); |
| } |
| |
| public static ChangeLogTokenResponse getChangeLogToken( |
| ChangeLogTokenRequest request, Context context) throws InterruptedException { |
| HealthConnectReceiver<ChangeLogTokenResponse> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager(context) |
| .getChangeLogToken(request, Executors.newSingleThreadExecutor(), receiver); |
| return receiver.getResponse(); |
| } |
| |
| public static String insertRecordAndGetId(Record record) throws InterruptedException { |
| return insertRecords(Collections.singletonList(record)).get(0).getMetadata().getId(); |
| } |
| |
| public static String insertRecordAndGetId(Record record, Context context) |
| throws InterruptedException { |
| return insertRecords(Collections.singletonList(record), context) |
| .get(0) |
| .getMetadata() |
| .getId(); |
| } |
| |
| /** |
| * Insert record to the database. |
| * |
| * @param record record to insert |
| * @return inserted record |
| */ |
| public static Record insertRecord(Record record) throws InterruptedException { |
| return insertRecords(Collections.singletonList(record)).get(0); |
| } |
| |
| /** |
| * Inserts records to the database. |
| * |
| * @param records records to insert |
| * @return inserted records |
| */ |
| public static List<Record> insertRecords(List<? extends Record> records) |
| throws InterruptedException { |
| return insertRecords(records, ApplicationProvider.getApplicationContext()); |
| } |
| |
| /** |
| * Inserts records to the database. |
| * |
| * @param records records to insert. |
| * @param context a {@link Context} to obtain {@link HealthConnectManager}. |
| * @return inserted records. |
| */ |
| public static List<Record> insertRecords(List<? extends Record> records, Context context) |
| throws InterruptedException { |
| HealthConnectReceiver<InsertRecordsResponse> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager(context) |
| .insertRecords( |
| unmodifiableList(records), Executors.newSingleThreadExecutor(), receiver); |
| List<Record> returnedRecords = receiver.getResponse().getRecords(); |
| assertThat(returnedRecords).hasSize(records.size()); |
| return returnedRecords; |
| } |
| |
| public static List<RecordTypeAndRecordIds> insertRecordsAndGetIds( |
| List<Record> records, Context context) throws InterruptedException { |
| List<Record> insertedRecords = insertRecords(records, context); |
| |
| Map<String, List<String>> recordTypeToRecordIdsMap = new HashMap<>(); |
| for (Record record : insertedRecords) { |
| recordTypeToRecordIdsMap.putIfAbsent(record.getClass().getName(), new ArrayList<>()); |
| recordTypeToRecordIdsMap |
| .get(record.getClass().getName()) |
| .add(record.getMetadata().getId()); |
| } |
| |
| List<RecordTypeAndRecordIds> recordTypeAndRecordIdsList = new ArrayList<>(); |
| for (String recordType : recordTypeToRecordIdsMap.keySet()) { |
| recordTypeAndRecordIdsList.add( |
| new RecordTypeAndRecordIds( |
| recordType, recordTypeToRecordIdsMap.get(recordType))); |
| } |
| |
| return recordTypeAndRecordIdsList; |
| } |
| |
| public static void updateRecords(List<Record> records) throws InterruptedException { |
| updateRecords(records, ApplicationProvider.getApplicationContext()); |
| } |
| |
| public static void updateRecords(List<Record> records, Context context) |
| throws InterruptedException { |
| HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager(context) |
| .updateRecords(records, Executors.newSingleThreadExecutor(), receiver); |
| receiver.verifyNoExceptionOrThrow(); |
| } |
| |
| public static ChangeLogsResponse getChangeLogs(ChangeLogsRequest changeLogsRequest) |
| throws InterruptedException { |
| return getChangeLogs(changeLogsRequest, ApplicationProvider.getApplicationContext()); |
| } |
| |
| public static ChangeLogsResponse getChangeLogs( |
| ChangeLogsRequest changeLogsRequest, Context context) throws InterruptedException { |
| HealthConnectReceiver<ChangeLogsResponse> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager(context) |
| .getChangeLogs(changeLogsRequest, Executors.newSingleThreadExecutor(), receiver); |
| return receiver.getResponse(); |
| } |
| |
| public static Device buildDevice() { |
| return new Device.Builder() |
| .setManufacturer("google") |
| .setModel("Pixel4a") |
| .setType(2) |
| .build(); |
| } |
| |
| private static Metadata buildSessionMetadata(String packageName, double clientId) { |
| Device device = |
| new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build(); |
| DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build(); |
| return new Metadata.Builder() |
| .setDevice(device) |
| .setDataOrigin(dataOrigin) |
| .setClientRecordId(String.valueOf(clientId)) |
| .build(); |
| } |
| |
| public static List<Record> getTestRecords() { |
| return Arrays.asList( |
| getStepsRecord(), |
| getHeartRateRecord(), |
| getBasalMetabolicRateRecord(), |
| buildExerciseSession()); |
| } |
| |
| public static List<Record> getTestRecords(String packageName) { |
| double clientId = Math.random(); |
| return getTestRecords(packageName, clientId); |
| } |
| |
| public static List<Record> getTestRecords(String packageName, Double clientId) { |
| return Arrays.asList( |
| getExerciseSessionRecord(packageName, clientId, /* withRoute= */ true), |
| getStepsRecord(packageName, clientId), |
| getHeartRateRecord(packageName, clientId), |
| getBasalMetabolicRateRecord(packageName, clientId)); |
| } |
| |
| public static List<RecordAndIdentifier> getRecordsAndIdentifiers() { |
| return Arrays.asList( |
| new RecordAndIdentifier(RECORD_TYPE_STEPS, getStepsRecord()), |
| new RecordAndIdentifier(RECORD_TYPE_HEART_RATE, getHeartRateRecord()), |
| new RecordAndIdentifier( |
| RECORD_TYPE_BASAL_METABOLIC_RATE, getBasalMetabolicRateRecord())); |
| } |
| |
| public static ExerciseRoute.Location buildLocationTimePoint(Instant startTime) { |
| return new ExerciseRoute.Location.Builder( |
| Instant.ofEpochMilli( |
| (long) (startTime.toEpochMilli() + 10 + Math.random() * 50)), |
| Math.random() * 50, |
| Math.random() * 50) |
| .build(); |
| } |
| |
| public static ExerciseRoute buildExerciseRoute() { |
| return new ExerciseRoute( |
| List.of( |
| buildLocationTimePoint(SESSION_START_TIME), |
| buildLocationTimePoint(SESSION_START_TIME), |
| buildLocationTimePoint(SESSION_START_TIME))); |
| } |
| |
| public static StepsRecord getStepsRecord() { |
| double clientId = Math.random(); |
| String packageName = ApplicationProvider.getApplicationContext().getPackageName(); |
| return getStepsRecord(packageName, clientId); |
| } |
| |
| public static StepsRecord getStepsRecord(String packageName, double clientId) { |
| Device device = |
| new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build(); |
| DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build(); |
| return new StepsRecord.Builder( |
| new Metadata.Builder() |
| .setDevice(device) |
| .setDataOrigin(dataOrigin) |
| .setClientRecordId("SR" + clientId) |
| .build(), |
| Instant.now(), |
| Instant.now().plusMillis(1000), |
| 10) |
| .build(); |
| } |
| |
| public static StepsRecord getStepsRecord(String id) { |
| Context context = ApplicationProvider.getApplicationContext(); |
| Device device = |
| new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build(); |
| DataOrigin dataOrigin = |
| new DataOrigin.Builder().setPackageName(context.getPackageName()).build(); |
| return new StepsRecord.Builder( |
| new Metadata.Builder() |
| .setDevice(device) |
| .setId(id) |
| .setDataOrigin(dataOrigin) |
| .build(), |
| Instant.now(), |
| Instant.now().plusMillis(1000), |
| 10) |
| .build(); |
| } |
| |
| public static HeartRateRecord getHeartRateRecord() { |
| String packageName = ApplicationProvider.getApplicationContext().getPackageName(); |
| double clientId = Math.random(); |
| return getHeartRateRecord(packageName, clientId); |
| } |
| |
| public static HeartRateRecord getHeartRateRecord(String packageName, double clientId) { |
| HeartRateRecord.HeartRateSample heartRateSample = |
| new HeartRateRecord.HeartRateSample(72, Instant.now().plusMillis(100)); |
| ArrayList<HeartRateRecord.HeartRateSample> heartRateSamples = new ArrayList<>(); |
| heartRateSamples.add(heartRateSample); |
| heartRateSamples.add(heartRateSample); |
| Device device = |
| new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build(); |
| DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build(); |
| |
| return new HeartRateRecord.Builder( |
| new Metadata.Builder() |
| .setDevice(device) |
| .setDataOrigin(dataOrigin) |
| .setClientRecordId("HR" + clientId) |
| .build(), |
| Instant.now(), |
| Instant.now().plusMillis(500), |
| heartRateSamples) |
| .build(); |
| } |
| |
| public static HeartRateRecord getHeartRateRecord(int heartRate) { |
| HeartRateRecord.HeartRateSample heartRateSample = |
| new HeartRateRecord.HeartRateSample(heartRate, Instant.now().plusMillis(100)); |
| ArrayList<HeartRateRecord.HeartRateSample> heartRateSamples = new ArrayList<>(); |
| heartRateSamples.add(heartRateSample); |
| heartRateSamples.add(heartRateSample); |
| |
| return new HeartRateRecord.Builder( |
| new Metadata.Builder().build(), |
| Instant.now(), |
| Instant.now().plusMillis(500), |
| heartRateSamples) |
| .build(); |
| } |
| |
| public static HeartRateRecord getHeartRateRecord(int heartRate, Instant instant) { |
| HeartRateRecord.HeartRateSample heartRateSample = |
| new HeartRateRecord.HeartRateSample(heartRate, instant); |
| ArrayList<HeartRateRecord.HeartRateSample> heartRateSamples = new ArrayList<>(); |
| heartRateSamples.add(heartRateSample); |
| heartRateSamples.add(heartRateSample); |
| |
| return new HeartRateRecord.Builder( |
| new Metadata.Builder().build(), |
| instant, |
| instant.plusMillis(1000), |
| heartRateSamples) |
| .build(); |
| } |
| |
| public static BasalMetabolicRateRecord getBasalMetabolicRateRecord() { |
| String packageName = ApplicationProvider.getApplicationContext().getPackageName(); |
| double clientId = Math.random(); |
| |
| return getBasalMetabolicRateRecord(packageName, clientId); |
| } |
| |
| public static BasalMetabolicRateRecord getBasalMetabolicRateRecord( |
| String packageName, double clientId) { |
| Device device = |
| new Device.Builder() |
| .setManufacturer("google") |
| .setModel("Pixel4a") |
| .setType(2) |
| .build(); |
| DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build(); |
| return new BasalMetabolicRateRecord.Builder( |
| new Metadata.Builder() |
| .setDevice(device) |
| .setDataOrigin(dataOrigin) |
| .setClientRecordId("BMR" + clientId) |
| .build(), |
| Instant.now(), |
| Power.fromWatts(100.0)) |
| .build(); |
| } |
| |
| public static ExerciseSessionRecord getExerciseSessionRecord( |
| String packageName, double clientId, boolean withRoute) { |
| Instant startTime = Instant.now().minusSeconds(3000).truncatedTo(ChronoUnit.MILLIS); |
| Instant endTime = Instant.now(); |
| ExerciseSessionRecord.Builder builder = |
| new ExerciseSessionRecord.Builder( |
| buildSessionMetadata(packageName, clientId), |
| startTime, |
| endTime, |
| ExerciseSessionType.EXERCISE_SESSION_TYPE_OTHER_WORKOUT) |
| .setEndZoneOffset(ZoneOffset.MAX) |
| .setStartZoneOffset(ZoneOffset.MIN) |
| .setNotes("notes") |
| .setTitle("title"); |
| |
| if (withRoute) { |
| builder.setRoute( |
| new ExerciseRoute( |
| List.of( |
| new ExerciseRoute.Location.Builder(startTime, 50., 50.).build(), |
| new ExerciseRoute.Location.Builder( |
| startTime.plusSeconds(2), 51., 51.) |
| .build()))); |
| } |
| return builder.build(); |
| } |
| |
| public static StepsRecord buildStepsRecord( |
| String startTime, String endTime, int stepsCount, String packageName) { |
| Device device = |
| new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build(); |
| DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build(); |
| return new StepsRecord.Builder( |
| new Metadata.Builder().setDevice(device).setDataOrigin(dataOrigin).build(), |
| getInstantTime(startTime), |
| getInstantTime(endTime), |
| stepsCount) |
| .build(); |
| } |
| |
| public static ExerciseSessionRecord buildExerciseSession( |
| String sessionStartTime, String sessionEndTime, Context context) { |
| return new ExerciseSessionRecord.Builder( |
| new Metadata.Builder() |
| .setDataOrigin( |
| new DataOrigin.Builder() |
| .setPackageName(context.getPackageName()) |
| .build()) |
| .setId("ExerciseSession" + Math.random()) |
| .setClientRecordId("ExerciseSessionClient" + Math.random()) |
| .build(), |
| getInstantTime(sessionStartTime), |
| getInstantTime(sessionEndTime), |
| ExerciseSessionType.EXERCISE_SESSION_TYPE_FOOTBALL_AMERICAN) |
| .build(); |
| } |
| |
| public static ExerciseSessionRecord buildExerciseSession( |
| String sessionStartTime, |
| String sessionEndTime, |
| String pauseStart, |
| String pauseEnd, |
| Context context) { |
| List<ExerciseSegment> segmentList = |
| List.of( |
| new ExerciseSegment.Builder( |
| getInstantTime(sessionStartTime), |
| getInstantTime(pauseStart), |
| ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_OTHER_WORKOUT) |
| .setRepetitionsCount(10) |
| .build(), |
| new ExerciseSegment.Builder( |
| getInstantTime(pauseStart), |
| getInstantTime(pauseEnd), |
| ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_PAUSE) |
| .build()); |
| |
| if (getInstantTime(sessionEndTime).compareTo(getInstantTime(pauseEnd)) > 0) { |
| segmentList.add( |
| new ExerciseSegment.Builder( |
| getInstantTime(pauseEnd), |
| getInstantTime(sessionEndTime), |
| ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_OTHER_WORKOUT) |
| .setRepetitionsCount(10) |
| .build()); |
| } |
| |
| return new ExerciseSessionRecord.Builder( |
| new Metadata.Builder() |
| .setDataOrigin( |
| new DataOrigin.Builder() |
| .setPackageName(context.getPackageName()) |
| .build()) |
| .setId("ExerciseSession" + Math.random()) |
| .setClientRecordId("ExerciseSessionClient" + Math.random()) |
| .build(), |
| getInstantTime(sessionStartTime), |
| getInstantTime(sessionEndTime), |
| ExerciseSessionType.EXERCISE_SESSION_TYPE_FOOTBALL_AMERICAN) |
| .setSegments(segmentList) |
| .build(); |
| } |
| |
| public static Instant getInstantTime(String time) { |
| return LocalDateTime.parse( |
| time + " Mon 5/15/2023", |
| DateTimeFormatter.ofPattern("hh:mm a EEE M/d/uuuu", Locale.US)) |
| .atZone(ZoneId.of("America/Toronto")) |
| .toInstant(); |
| } |
| |
| public static <T> AggregateRecordsResponse<T> getAggregateResponse( |
| AggregateRecordsRequest<T> request) throws InterruptedException { |
| HealthConnectReceiver<AggregateRecordsResponse<T>> receiver = |
| new HealthConnectReceiver<AggregateRecordsResponse<T>>(); |
| getHealthConnectManager().aggregate(request, Executors.newSingleThreadExecutor(), receiver); |
| return receiver.getResponse(); |
| } |
| |
| public static <T> AggregateRecordsResponse<T> getAggregateResponse( |
| AggregateRecordsRequest<T> request, List<Record> recordsToInsert) |
| throws InterruptedException { |
| if (recordsToInsert != null) { |
| insertRecords(recordsToInsert); |
| } |
| |
| HealthConnectReceiver<AggregateRecordsResponse<T>> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager().aggregate(request, Executors.newSingleThreadExecutor(), receiver); |
| return receiver.getResponse(); |
| } |
| |
| public static <T> |
| List<AggregateRecordsGroupedByDurationResponse<T>> getAggregateResponseGroupByDuration( |
| AggregateRecordsRequest<T> request, Duration duration) |
| throws InterruptedException { |
| HealthConnectReceiver<List<AggregateRecordsGroupedByDurationResponse<T>>> receiver = |
| new HealthConnectReceiver<>(); |
| getHealthConnectManager() |
| .aggregateGroupByDuration( |
| request, duration, Executors.newSingleThreadExecutor(), receiver); |
| return receiver.getResponse(); |
| } |
| |
| public static <T> |
| List<AggregateRecordsGroupedByPeriodResponse<T>> getAggregateResponseGroupByPeriod( |
| AggregateRecordsRequest<T> request, Period period) throws InterruptedException { |
| HealthConnectReceiver<List<AggregateRecordsGroupedByPeriodResponse<T>>> receiver = |
| new HealthConnectReceiver<>(); |
| getHealthConnectManager() |
| .aggregateGroupByPeriod( |
| request, period, Executors.newSingleThreadExecutor(), receiver); |
| return receiver.getResponse(); |
| } |
| |
| public static <T extends Record> List<T> readRecords(ReadRecordsRequest<T> request) |
| throws InterruptedException { |
| return readRecords(request, ApplicationProvider.getApplicationContext()); |
| } |
| |
| public static <T extends Record> List<T> readRecords( |
| ReadRecordsRequest<T> request, Context context) throws InterruptedException { |
| assertThat(request.getRecordType()).isNotNull(); |
| HealthConnectReceiver<ReadRecordsResponse<T>> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager(context) |
| .readRecords(request, Executors.newSingleThreadExecutor(), receiver); |
| return receiver.getResponse().getRecords(); |
| } |
| |
| public static <T extends Record> void assertRecordNotFound(String uuid, Class<T> recordType) |
| throws InterruptedException { |
| assertThat( |
| readRecords( |
| new ReadRecordsRequestUsingIds.Builder<>(recordType) |
| .addId(uuid) |
| .build())) |
| .isEmpty(); |
| } |
| |
| public static <T extends Record> void assertRecordFound(String uuid, Class<T> recordType) |
| throws InterruptedException { |
| assertThat( |
| readRecords( |
| new ReadRecordsRequestUsingIds.Builder<>(recordType) |
| .addId(uuid) |
| .build())) |
| .isNotEmpty(); |
| } |
| |
| /** Reads all records in the DB for a given {@code recordClass}. */ |
| public static <T extends Record> List<T> readAllRecords(Class<T> recordClass) |
| throws InterruptedException { |
| List<T> records = new ArrayList<>(); |
| ReadRecordsResponse<T> readRecordsResponse = |
| readRecordsWithPagination( |
| new ReadRecordsRequestUsingFilters.Builder<>(recordClass).build()); |
| while (true) { |
| records.addAll(readRecordsResponse.getRecords()); |
| long pageToken = readRecordsResponse.getNextPageToken(); |
| if (pageToken == -1) { |
| break; |
| } |
| readRecordsResponse = |
| readRecordsWithPagination( |
| new ReadRecordsRequestUsingFilters.Builder<>(recordClass) |
| .setPageToken(pageToken) |
| .build()); |
| } |
| return records; |
| } |
| |
| public static <T extends Record> ReadRecordsResponse<T> readRecordsWithPagination( |
| ReadRecordsRequest<T> request) throws InterruptedException { |
| HealthConnectReceiver<ReadRecordsResponse<T>> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager() |
| .readRecords(request, Executors.newSingleThreadExecutor(), receiver); |
| return receiver.getResponse(); |
| } |
| |
| public static void setAutoDeletePeriod(int period) throws InterruptedException { |
| UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); |
| uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA); |
| try { |
| HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager() |
| .setRecordRetentionPeriodInDays( |
| period, Executors.newSingleThreadExecutor(), receiver); |
| receiver.verifyNoExceptionOrThrow(); |
| } finally { |
| uiAutomation.dropShellPermissionIdentity(); |
| } |
| } |
| |
| public static void verifyDeleteRecords(DeleteUsingFiltersRequest request) |
| throws InterruptedException { |
| UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); |
| uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA); |
| try { |
| HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager() |
| .deleteRecords(request, Executors.newSingleThreadExecutor(), receiver); |
| receiver.verifyNoExceptionOrThrow(); |
| } finally { |
| uiAutomation.dropShellPermissionIdentity(); |
| } |
| } |
| |
| public static void verifyDeleteRecords(List<RecordIdFilter> request) |
| throws InterruptedException { |
| verifyDeleteRecords(request, ApplicationProvider.getApplicationContext()); |
| } |
| |
| public static void verifyDeleteRecords(List<RecordIdFilter> request, Context context) |
| throws InterruptedException { |
| HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager(context) |
| .deleteRecords(request, Executors.newSingleThreadExecutor(), receiver); |
| receiver.verifyNoExceptionOrThrow(); |
| } |
| |
| public static void verifyDeleteRecords( |
| Class<? extends Record> recordType, TimeInstantRangeFilter timeRangeFilter) |
| throws InterruptedException { |
| HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager() |
| .deleteRecords( |
| recordType, timeRangeFilter, Executors.newSingleThreadExecutor(), receiver); |
| receiver.verifyNoExceptionOrThrow(); |
| } |
| |
| /** Helper function to delete records from the DB using HealthConnectManager. */ |
| public static void deleteRecords(List<? extends Record> records) throws InterruptedException { |
| List<RecordIdFilter> recordIdFilters = |
| records.stream() |
| .map( |
| (record -> |
| RecordIdFilter.fromId( |
| record.getClass(), record.getMetadata().getId()))) |
| .collect(Collectors.toList()); |
| verifyDeleteRecords(recordIdFilters); |
| } |
| |
| public static List<AccessLog> queryAccessLogs() throws InterruptedException { |
| UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); |
| uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA); |
| try { |
| HealthConnectReceiver<List<AccessLog>> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager() |
| .queryAccessLogs(Executors.newSingleThreadExecutor(), receiver); |
| return receiver.getResponse(); |
| } finally { |
| uiAutomation.dropShellPermissionIdentity(); |
| } |
| } |
| |
| public static Map<Class<? extends Record>, RecordTypeInfoResponse> queryAllRecordTypesInfo() |
| throws InterruptedException, NullPointerException { |
| UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); |
| uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA); |
| try { |
| HealthConnectReceiver<Map<Class<? extends Record>, RecordTypeInfoResponse>> receiver = |
| new HealthConnectReceiver<>(); |
| getHealthConnectManager() |
| .queryAllRecordTypesInfo(Executors.newSingleThreadExecutor(), receiver); |
| return receiver.getResponse(); |
| } finally { |
| uiAutomation.dropShellPermissionIdentity(); |
| } |
| } |
| |
| public static List<LocalDate> getActivityDates(List<Class<? extends Record>> recordTypes) |
| throws InterruptedException { |
| UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); |
| uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA); |
| try { |
| HealthConnectReceiver<List<LocalDate>> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager() |
| .queryActivityDates(recordTypes, Executors.newSingleThreadExecutor(), receiver); |
| return receiver.getResponse(); |
| } finally { |
| uiAutomation.dropShellPermissionIdentity(); |
| } |
| } |
| |
| public static ExerciseSessionRecord buildExerciseSession() { |
| return buildExerciseSession(buildExerciseRoute(), "Morning training", "rain"); |
| } |
| |
| public static SleepSessionRecord buildSleepSession() { |
| return new SleepSessionRecord.Builder( |
| generateMetadata(), SESSION_START_TIME, SESSION_END_TIME) |
| .setNotes("warm") |
| .setTitle("Afternoon nap") |
| .setStages( |
| List.of( |
| new SleepSessionRecord.Stage( |
| SESSION_START_TIME, |
| SESSION_START_TIME.plusSeconds(300), |
| SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_LIGHT), |
| new SleepSessionRecord.Stage( |
| SESSION_START_TIME.plusSeconds(300), |
| SESSION_START_TIME.plusSeconds(600), |
| SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_REM), |
| new SleepSessionRecord.Stage( |
| SESSION_START_TIME.plusSeconds(900), |
| SESSION_START_TIME.plusSeconds(1200), |
| SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_DEEP))) |
| .build(); |
| } |
| |
| public static void startMigration() throws InterruptedException { |
| MigrationReceiver receiver = new MigrationReceiver(); |
| getHealthConnectManager().startMigration(Executors.newSingleThreadExecutor(), receiver); |
| receiver.verifyNoExceptionOrThrow(); |
| } |
| |
| public static void writeMigrationData(List<MigrationEntity> entities) |
| throws InterruptedException { |
| MigrationReceiver receiver = new MigrationReceiver(); |
| getHealthConnectManager() |
| .writeMigrationData(entities, Executors.newSingleThreadExecutor(), receiver); |
| receiver.verifyNoExceptionOrThrow(); |
| } |
| |
| public static void finishMigration() throws InterruptedException { |
| MigrationReceiver receiver = new MigrationReceiver(); |
| getHealthConnectManager().finishMigration(Executors.newSingleThreadExecutor(), receiver); |
| receiver.verifyNoExceptionOrThrow(); |
| } |
| |
| public static void insertMinDataMigrationSdkExtensionVersion(int version) |
| throws InterruptedException { |
| MigrationReceiver receiver = new MigrationReceiver(); |
| getHealthConnectManager() |
| .insertMinDataMigrationSdkExtensionVersion( |
| version, Executors.newSingleThreadExecutor(), receiver); |
| receiver.verifyNoExceptionOrThrow(); |
| } |
| |
| public static void deleteAllStagedRemoteData() { |
| HealthConnectManager service = getHealthConnectManager(); |
| runWithShellPermissionIdentity( |
| () -> |
| // TODO(b/241542162): Avoid reflection once TestApi can be called from CTS |
| service.getClass().getMethod("deleteAllStagedRemoteData").invoke(service), |
| "android.permission.DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA"); |
| } |
| |
| public static int getHealthConnectDataMigrationState() throws InterruptedException { |
| HealthConnectReceiver<HealthConnectDataState> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager() |
| .getHealthConnectDataState(Executors.newSingleThreadExecutor(), receiver); |
| return receiver.getResponse().getDataMigrationState(); |
| } |
| |
| public static int getHealthConnectDataRestoreState() throws InterruptedException { |
| HealthConnectReceiver<HealthConnectDataState> receiver = new HealthConnectReceiver<>(); |
| runWithShellPermissionIdentity( |
| () -> |
| getHealthConnectManager() |
| .getHealthConnectDataState( |
| Executors.newSingleThreadExecutor(), receiver), |
| MANAGE_HEALTH_DATA); |
| return receiver.getResponse().getDataRestoreState(); |
| } |
| |
| public static List<AppInfo> getApplicationInfo() throws InterruptedException { |
| HealthConnectReceiver<ApplicationInfoResponse> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager() |
| .getContributorApplicationsInfo(Executors.newSingleThreadExecutor(), receiver); |
| return receiver.getResponse().getApplicationInfoList(); |
| } |
| |
| public static <T extends Record> T getRecordById(List<T> list, String id) { |
| for (T record : list) { |
| if (record.getMetadata().getId().equals(id)) { |
| return record; |
| } |
| } |
| |
| throw new AssertionError("Record not found with id: " + id); |
| } |
| |
| public static Metadata generateMetadata() { |
| Context context = ApplicationProvider.getApplicationContext(); |
| return new Metadata.Builder() |
| .setDevice(buildDevice()) |
| .setId(UUID.randomUUID().toString()) |
| .setClientRecordId("clientRecordId" + Math.random()) |
| .setDataOrigin( |
| new DataOrigin.Builder().setPackageName(context.getPackageName()).build()) |
| .setDevice(buildDevice()) |
| .setRecordingMethod(Metadata.RECORDING_METHOD_UNKNOWN) |
| .build(); |
| } |
| |
| public static HeartRateRecord getHugeHeartRateRecord() { |
| Device device = |
| new Device.Builder() |
| .setManufacturer("google") |
| .setModel("Pixel4a") |
| .setType(2) |
| .build(); |
| DataOrigin dataOrigin = |
| new DataOrigin.Builder().setPackageName("android.healthconnect.cts").build(); |
| Metadata.Builder testMetadataBuilder = new Metadata.Builder(); |
| testMetadataBuilder.setDevice(device).setDataOrigin(dataOrigin); |
| testMetadataBuilder.setClientRecordId("HRR" + Math.random()); |
| testMetadataBuilder.setRecordingMethod(Metadata.RECORDING_METHOD_ACTIVELY_RECORDED); |
| |
| HeartRateRecord.HeartRateSample heartRateRecord = |
| new HeartRateRecord.HeartRateSample(10, Instant.now().plusMillis(100)); |
| ArrayList<HeartRateRecord.HeartRateSample> heartRateRecords = |
| new ArrayList<>(Collections.nCopies(85000, heartRateRecord)); |
| |
| return new HeartRateRecord.Builder( |
| testMetadataBuilder.build(), |
| Instant.now(), |
| Instant.now().plusMillis(500), |
| heartRateRecords) |
| .build(); |
| } |
| |
| public static StepsRecord getCompleteStepsRecord() { |
| Device device = |
| new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build(); |
| DataOrigin dataOrigin = |
| new DataOrigin.Builder().setPackageName("android.healthconnect.cts").build(); |
| |
| Metadata.Builder testMetadataBuilder = new Metadata.Builder(); |
| testMetadataBuilder.setDevice(device).setDataOrigin(dataOrigin); |
| testMetadataBuilder.setClientRecordId("SR" + Math.random()); |
| testMetadataBuilder.setRecordingMethod(RECORDING_METHOD_ACTIVELY_RECORDED); |
| Metadata testMetaData = testMetadataBuilder.build(); |
| assertThat(testMetaData.getRecordingMethod()).isEqualTo(RECORDING_METHOD_ACTIVELY_RECORDED); |
| return new StepsRecord.Builder( |
| testMetaData, Instant.now(), Instant.now().plusMillis(1000), 10) |
| .build(); |
| } |
| |
| public static StepsRecord getStepsRecord_update( |
| Record record, String id, String clientRecordId) { |
| Metadata metadata = record.getMetadata(); |
| Metadata metadataWithId = |
| new Metadata.Builder() |
| .setId(id) |
| .setClientRecordId(clientRecordId) |
| .setClientRecordVersion(metadata.getClientRecordVersion()) |
| .setDataOrigin(metadata.getDataOrigin()) |
| .setDevice(metadata.getDevice()) |
| .setLastModifiedTime(metadata.getLastModifiedTime()) |
| .build(); |
| return new StepsRecord.Builder( |
| metadataWithId, Instant.now(), Instant.now().plusMillis(2000), 20) |
| .setStartZoneOffset(ZoneOffset.systemDefault().getRules().getOffset(Instant.now())) |
| .setEndZoneOffset(ZoneOffset.systemDefault().getRules().getOffset(Instant.now())) |
| .build(); |
| } |
| |
| private static ExerciseSessionRecord buildExerciseSession( |
| ExerciseRoute route, String title, String notes) { |
| return new ExerciseSessionRecord.Builder( |
| generateMetadata(), |
| SESSION_START_TIME, |
| SESSION_END_TIME, |
| ExerciseSessionType.EXERCISE_SESSION_TYPE_OTHER_WORKOUT) |
| .setRoute(route) |
| .setLaps( |
| List.of( |
| new ExerciseLap.Builder( |
| SESSION_START_TIME, |
| SESSION_START_TIME.plusSeconds(20)) |
| .setLength(Length.fromMeters(10)) |
| .build(), |
| new ExerciseLap.Builder( |
| SESSION_END_TIME.minusSeconds(20), SESSION_END_TIME) |
| .build())) |
| .setSegments( |
| List.of( |
| new ExerciseSegment.Builder( |
| SESSION_START_TIME.plusSeconds(1), |
| SESSION_START_TIME.plusSeconds(10), |
| ExerciseSegmentType |
| .EXERCISE_SEGMENT_TYPE_BENCH_PRESS) |
| .build(), |
| new ExerciseSegment.Builder( |
| SESSION_START_TIME.plusSeconds(21), |
| SESSION_START_TIME.plusSeconds(124), |
| ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_BURPEE) |
| .setRepetitionsCount(15) |
| .build())) |
| .setEndZoneOffset(ZoneOffset.MAX) |
| .setStartZoneOffset(ZoneOffset.MIN) |
| .setNotes(notes) |
| .setTitle(title) |
| .build(); |
| } |
| |
| public static void populateAndResetExpectedResponseMap( |
| HashMap<Class<? extends Record>, RecordTypeInfoTestResponse> expectedResponseMap) { |
| expectedResponseMap.put( |
| ElevationGainedRecord.class, |
| new RecordTypeInfoTestResponse( |
| ACTIVITY, HealthPermissionCategory.ELEVATION_GAINED, new ArrayList<>())); |
| expectedResponseMap.put( |
| OvulationTestRecord.class, |
| new RecordTypeInfoTestResponse( |
| CYCLE_TRACKING, |
| HealthPermissionCategory.OVULATION_TEST, |
| new ArrayList<>())); |
| expectedResponseMap.put( |
| DistanceRecord.class, |
| new RecordTypeInfoTestResponse( |
| ACTIVITY, HealthPermissionCategory.DISTANCE, new ArrayList<>())); |
| expectedResponseMap.put( |
| SpeedRecord.class, |
| new RecordTypeInfoTestResponse( |
| ACTIVITY, HealthPermissionCategory.SPEED, new ArrayList<>())); |
| |
| expectedResponseMap.put( |
| Vo2MaxRecord.class, |
| new RecordTypeInfoTestResponse( |
| ACTIVITY, HealthPermissionCategory.VO2_MAX, new ArrayList<>())); |
| expectedResponseMap.put( |
| OxygenSaturationRecord.class, |
| new RecordTypeInfoTestResponse( |
| VITALS, HealthPermissionCategory.OXYGEN_SATURATION, new ArrayList<>())); |
| expectedResponseMap.put( |
| TotalCaloriesBurnedRecord.class, |
| new RecordTypeInfoTestResponse( |
| ACTIVITY, |
| HealthPermissionCategory.TOTAL_CALORIES_BURNED, |
| new ArrayList<>())); |
| expectedResponseMap.put( |
| HydrationRecord.class, |
| new RecordTypeInfoTestResponse( |
| NUTRITION, HealthPermissionCategory.HYDRATION, new ArrayList<>())); |
| expectedResponseMap.put( |
| StepsRecord.class, |
| new RecordTypeInfoTestResponse(ACTIVITY, STEPS, new ArrayList<>())); |
| expectedResponseMap.put( |
| CervicalMucusRecord.class, |
| new RecordTypeInfoTestResponse( |
| CYCLE_TRACKING, |
| HealthPermissionCategory.CERVICAL_MUCUS, |
| new ArrayList<>())); |
| expectedResponseMap.put( |
| ExerciseSessionRecord.class, |
| new RecordTypeInfoTestResponse(ACTIVITY, EXERCISE, new ArrayList<>())); |
| expectedResponseMap.put( |
| HeartRateRecord.class, |
| new RecordTypeInfoTestResponse(VITALS, HEART_RATE, new ArrayList<>())); |
| expectedResponseMap.put( |
| RespiratoryRateRecord.class, |
| new RecordTypeInfoTestResponse( |
| VITALS, HealthPermissionCategory.RESPIRATORY_RATE, new ArrayList<>())); |
| expectedResponseMap.put( |
| BasalBodyTemperatureRecord.class, |
| new RecordTypeInfoTestResponse( |
| VITALS, |
| HealthPermissionCategory.BASAL_BODY_TEMPERATURE, |
| new ArrayList<>())); |
| expectedResponseMap.put( |
| WheelchairPushesRecord.class, |
| new RecordTypeInfoTestResponse( |
| ACTIVITY, HealthPermissionCategory.WHEELCHAIR_PUSHES, new ArrayList<>())); |
| expectedResponseMap.put( |
| PowerRecord.class, |
| new RecordTypeInfoTestResponse( |
| ACTIVITY, HealthPermissionCategory.POWER, new ArrayList<>())); |
| expectedResponseMap.put( |
| BodyWaterMassRecord.class, |
| new RecordTypeInfoTestResponse( |
| BODY_MEASUREMENTS, |
| HealthPermissionCategory.BODY_WATER_MASS, |
| new ArrayList<>())); |
| expectedResponseMap.put( |
| WeightRecord.class, |
| new RecordTypeInfoTestResponse( |
| BODY_MEASUREMENTS, HealthPermissionCategory.WEIGHT, new ArrayList<>())); |
| expectedResponseMap.put( |
| BoneMassRecord.class, |
| new RecordTypeInfoTestResponse( |
| BODY_MEASUREMENTS, HealthPermissionCategory.BONE_MASS, new ArrayList<>())); |
| expectedResponseMap.put( |
| RestingHeartRateRecord.class, |
| new RecordTypeInfoTestResponse( |
| VITALS, HealthPermissionCategory.RESTING_HEART_RATE, new ArrayList<>())); |
| expectedResponseMap.put( |
| ActiveCaloriesBurnedRecord.class, |
| new RecordTypeInfoTestResponse( |
| ACTIVITY, |
| HealthPermissionCategory.ACTIVE_CALORIES_BURNED, |
| new ArrayList<>())); |
| expectedResponseMap.put( |
| BodyFatRecord.class, |
| new RecordTypeInfoTestResponse( |
| BODY_MEASUREMENTS, HealthPermissionCategory.BODY_FAT, new ArrayList<>())); |
| expectedResponseMap.put( |
| BodyTemperatureRecord.class, |
| new RecordTypeInfoTestResponse( |
| VITALS, HealthPermissionCategory.BODY_TEMPERATURE, new ArrayList<>())); |
| expectedResponseMap.put( |
| NutritionRecord.class, |
| new RecordTypeInfoTestResponse( |
| NUTRITION, HealthPermissionCategory.NUTRITION, new ArrayList<>())); |
| expectedResponseMap.put( |
| LeanBodyMassRecord.class, |
| new RecordTypeInfoTestResponse( |
| BODY_MEASUREMENTS, |
| HealthPermissionCategory.LEAN_BODY_MASS, |
| new ArrayList<>())); |
| expectedResponseMap.put( |
| HeartRateVariabilityRmssdRecord.class, |
| new RecordTypeInfoTestResponse( |
| VITALS, |
| HealthPermissionCategory.HEART_RATE_VARIABILITY, |
| new ArrayList<>())); |
| expectedResponseMap.put( |
| MenstruationFlowRecord.class, |
| new RecordTypeInfoTestResponse( |
| CYCLE_TRACKING, HealthPermissionCategory.MENSTRUATION, new ArrayList<>())); |
| expectedResponseMap.put( |
| BloodGlucoseRecord.class, |
| new RecordTypeInfoTestResponse( |
| VITALS, HealthPermissionCategory.BLOOD_GLUCOSE, new ArrayList<>())); |
| expectedResponseMap.put( |
| BloodPressureRecord.class, |
| new RecordTypeInfoTestResponse( |
| VITALS, HealthPermissionCategory.BLOOD_PRESSURE, new ArrayList<>())); |
| expectedResponseMap.put( |
| CyclingPedalingCadenceRecord.class, |
| new RecordTypeInfoTestResponse(ACTIVITY, EXERCISE, new ArrayList<>())); |
| expectedResponseMap.put( |
| IntermenstrualBleedingRecord.class, |
| new RecordTypeInfoTestResponse( |
| CYCLE_TRACKING, |
| HealthPermissionCategory.INTERMENSTRUAL_BLEEDING, |
| new ArrayList<>())); |
| expectedResponseMap.put( |
| FloorsClimbedRecord.class, |
| new RecordTypeInfoTestResponse( |
| ACTIVITY, HealthPermissionCategory.FLOORS_CLIMBED, new ArrayList<>())); |
| expectedResponseMap.put( |
| StepsCadenceRecord.class, |
| new RecordTypeInfoTestResponse(ACTIVITY, STEPS, new ArrayList<>())); |
| expectedResponseMap.put( |
| HeightRecord.class, |
| new RecordTypeInfoTestResponse( |
| BODY_MEASUREMENTS, HealthPermissionCategory.HEIGHT, new ArrayList<>())); |
| expectedResponseMap.put( |
| SexualActivityRecord.class, |
| new RecordTypeInfoTestResponse( |
| CYCLE_TRACKING, |
| HealthPermissionCategory.SEXUAL_ACTIVITY, |
| new ArrayList<>())); |
| expectedResponseMap.put( |
| MenstruationPeriodRecord.class, |
| new RecordTypeInfoTestResponse( |
| CYCLE_TRACKING, HealthPermissionCategory.MENSTRUATION, new ArrayList<>())); |
| expectedResponseMap.put( |
| SleepSessionRecord.class, |
| new RecordTypeInfoTestResponse( |
| SLEEP, HealthPermissionCategory.SLEEP, new ArrayList<>())); |
| expectedResponseMap.put( |
| BasalMetabolicRateRecord.class, |
| new RecordTypeInfoTestResponse( |
| BODY_MEASUREMENTS, BASAL_METABOLIC_RATE, new ArrayList<>())); |
| } |
| |
| public static FetchDataOriginsPriorityOrderResponse fetchDataOriginsPriorityOrder( |
| int dataCategory) throws InterruptedException { |
| HealthConnectReceiver<FetchDataOriginsPriorityOrderResponse> receiver = |
| new HealthConnectReceiver<>(); |
| getHealthConnectManager() |
| .fetchDataOriginsPriorityOrder( |
| dataCategory, Executors.newSingleThreadExecutor(), receiver); |
| return receiver.getResponse(); |
| } |
| |
| public static void updateDataOriginPriorityOrder(UpdateDataOriginPriorityOrderRequest request) |
| throws InterruptedException { |
| HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>(); |
| getHealthConnectManager() |
| .updateDataOriginPriorityOrder( |
| request, Executors.newSingleThreadExecutor(), receiver); |
| receiver.verifyNoExceptionOrThrow(); |
| } |
| |
| public static void grantPermission(String pkgName, String permission) { |
| HealthConnectManager service = getHealthConnectManager(); |
| runWithShellPermissionIdentity( |
| () -> |
| service.getClass() |
| .getMethod("grantHealthPermission", String.class, String.class) |
| .invoke(service, pkgName, permission), |
| MANAGE_HEALTH_PERMISSIONS); |
| } |
| |
| public static void revokePermission(String pkgName, String permission) { |
| HealthConnectManager service = getHealthConnectManager(); |
| runWithShellPermissionIdentity( |
| () -> |
| service.getClass() |
| .getMethod( |
| "revokeHealthPermission", |
| String.class, |
| String.class, |
| String.class) |
| .invoke(service, pkgName, permission, null), |
| MANAGE_HEALTH_PERMISSIONS); |
| } |
| |
| /** |
| * Utility method to call {@link HealthConnectManager#revokeAllHealthPermissions(String, |
| * String)}. |
| */ |
| public static void revokeAllPermissions(String packageName, @Nullable String reason) { |
| HealthConnectManager service = getHealthConnectManager(); |
| runWithShellPermissionIdentity( |
| () -> |
| service.getClass() |
| .getMethod("revokeAllHealthPermissions", String.class, String.class) |
| .invoke(service, packageName, reason), |
| MANAGE_HEALTH_PERMISSIONS); |
| } |
| |
| /** |
| * Same as {@link #revokeAllPermissions(String, String)} but with a delay to wait for grant time |
| * to be updated. |
| */ |
| public static void revokeAllPermissionsWithDelay(String packageName, @Nullable String reason) |
| throws InterruptedException { |
| revokeAllPermissions(packageName, reason); |
| Thread.sleep(500); |
| } |
| |
| /** |
| * Utility method to call {@link |
| * HealthConnectManager#getHealthDataHistoricalAccessStartDate(String)}. |
| */ |
| public static Instant getHealthDataHistoricalAccessStartDate(String packageName) { |
| HealthConnectManager service = getHealthConnectManager(); |
| return (Instant) |
| runWithShellPermissionIdentity( |
| () -> |
| service.getClass() |
| .getMethod( |
| "getHealthDataHistoricalAccessStartDate", |
| String.class) |
| .invoke(service, packageName), |
| MANAGE_HEALTH_PERMISSIONS); |
| } |
| |
| public static void revokeHealthPermissions(String packageName) { |
| runWithShellPermissionIdentity(() -> revokeHealthPermissionsPrivileged(packageName)); |
| } |
| |
| private static void revokeHealthPermissionsPrivileged(String packageName) |
| throws PackageManager.NameNotFoundException { |
| final Context targetContext = androidx.test.InstrumentationRegistry.getTargetContext(); |
| final PackageManager packageManager = targetContext.getPackageManager(); |
| final UserHandle user = targetContext.getUser(); |
| |
| final PackageInfo packageInfo = |
| packageManager.getPackageInfo( |
| packageName, |
| PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS)); |
| |
| final String[] permissions = packageInfo.requestedPermissions; |
| if (permissions == null) { |
| return; |
| } |
| |
| for (String permission : permissions) { |
| if (permission.startsWith(HEALTH_PERMISSION_PREFIX)) { |
| packageManager.revokeRuntimePermission(packageName, permission, user); |
| } |
| } |
| } |
| |
| public static List<String> getGrantedHealthPermissions(String pkgName) { |
| final PackageInfo pi = getAppPackageInfo(pkgName); |
| final String[] requestedPermissions = pi.requestedPermissions; |
| final int[] requestedPermissionsFlags = pi.requestedPermissionsFlags; |
| |
| if (requestedPermissions == null) { |
| return List.of(); |
| } |
| |
| final List<String> permissions = new ArrayList<>(); |
| |
| for (int i = 0; i < requestedPermissions.length; i++) { |
| if ((requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0) { |
| if (requestedPermissions[i].startsWith(HEALTH_PERMISSION_PREFIX)) { |
| permissions.add(requestedPermissions[i]); |
| } |
| } |
| } |
| |
| return permissions; |
| } |
| |
| private static PackageInfo getAppPackageInfo(String pkgName) { |
| final Context targetContext = androidx.test.InstrumentationRegistry.getTargetContext(); |
| return runWithShellPermissionIdentity( |
| () -> |
| targetContext |
| .getPackageManager() |
| .getPackageInfo( |
| pkgName, |
| PackageManager.PackageInfoFlags.of(GET_PERMISSIONS))); |
| } |
| |
| public static void deleteTestData() throws InterruptedException { |
| verifyDeleteRecords( |
| new DeleteUsingFiltersRequest.Builder() |
| .setTimeRangeFilter( |
| new TimeInstantRangeFilter.Builder() |
| .setStartTime(Instant.EPOCH) |
| .setEndTime(Instant.now().plus(10, ChronoUnit.DAYS)) |
| .build()) |
| .addRecordType(ExerciseSessionRecord.class) |
| .addRecordType(StepsRecord.class) |
| .addRecordType(HeartRateRecord.class) |
| .addRecordType(BasalMetabolicRateRecord.class) |
| .build()); |
| } |
| |
| public static void revokeAndThenGrantHealthPermissions(TestApp testApp) { |
| List<String> healthPerms = getGrantedHealthPermissions(testApp.getPackageName()); |
| |
| revokeHealthPermissions(testApp.getPackageName()); |
| |
| for (String perm : healthPerms) { |
| grantPermission(testApp.getPackageName(), perm); |
| } |
| } |
| |
| public static String runShellCommand(String command) throws IOException { |
| UiAutomation uiAutomation = |
| androidx.test.platform.app.InstrumentationRegistry.getInstrumentation() |
| .getUiAutomation(); |
| uiAutomation.adoptShellPermissionIdentity(); |
| final ParcelFileDescriptor stdout = uiAutomation.executeShellCommand(command); |
| StringBuilder output = new StringBuilder(); |
| |
| try (BufferedReader reader = |
| new BufferedReader( |
| new InputStreamReader(new FileInputStream(stdout.getFileDescriptor())))) { |
| char[] buffer = new char[4096]; |
| int bytesRead; |
| while ((bytesRead = reader.read(buffer)) != -1) { |
| output.append(buffer, 0, bytesRead); |
| } |
| } catch (FileNotFoundException e) { |
| Log.e(TAG, e.getMessage()); |
| } |
| |
| return output.toString(); |
| } |
| |
| @NonNull |
| private static HealthConnectManager getHealthConnectManager() { |
| return getHealthConnectManager(ApplicationProvider.getApplicationContext()); |
| } |
| |
| @NonNull |
| private static HealthConnectManager getHealthConnectManager(Context context) { |
| return requireNonNull(context.getSystemService(HealthConnectManager.class)); |
| } |
| |
| public static String getDeviceConfigValue(String key) { |
| return SystemUtil.runShellCommand("device_config get health_fitness " + key); |
| } |
| |
| public static void setDeviceConfigValue(String key, String value) { |
| SystemUtil.runShellCommand("device_config put health_fitness " + key + " " + value); |
| } |
| |
| public static void sendCommandToTestAppReceiver(Context context, String action) { |
| sendCommandToTestAppReceiver(context, action, /* extras= */ null); |
| } |
| |
| public static void sendCommandToTestAppReceiver(Context context, String action, Bundle extras) { |
| final Intent intent = new Intent(action).setClassName(PKG_TEST_APP, TEST_APP_RECEIVER); |
| intent.putExtra(EXTRA_SENDER_PACKAGE_NAME, context.getPackageName()); |
| if (extras != null) { |
| intent.putExtras(extras); |
| } |
| context.sendBroadcast(intent); |
| } |
| |
| /** Sets up the priority list for aggregation tests. */ |
| public static void setupAggregation(String packageName, int dataCategory) |
| throws InterruptedException { |
| insertRecordsForPriority(packageName); |
| |
| // Add the packageName inserting the records to the priority list manually |
| // Since CTS tests get their permissions granted at install time and skip |
| // the Health Connect APIs that would otherwise add the packageName to the priority list |
| |
| updatePriorityWithManageHealthDataPermission(dataCategory, Arrays.asList(packageName)); |
| FetchDataOriginsPriorityOrderResponse newPriority = |
| getPriorityWithManageHealthDataPermission(dataCategory); |
| List<String> newPriorityString = |
| newPriority.getDataOriginsPriorityOrder().stream() |
| .map(DataOrigin::getPackageName) |
| .toList(); |
| assertThat(newPriorityString.size()).isEqualTo(1); |
| assertThat(newPriorityString.get(0)).isEqualTo(packageName); |
| } |
| |
| /** Inserts a record that does not support aggregation to enable the priority list. */ |
| public static void insertRecordsForPriority(String packageName) throws InterruptedException { |
| // Insert records that do not support aggregation so that the AppInfoTable is initialised |
| MenstruationPeriodRecord recordToInsert = |
| new MenstruationPeriodRecord.Builder( |
| new Metadata.Builder() |
| .setDataOrigin( |
| new DataOrigin.Builder() |
| .setPackageName(packageName) |
| .build()) |
| .build(), |
| Instant.now(), |
| Instant.now().plusMillis(1000)) |
| .build(); |
| insertRecords(Arrays.asList(recordToInsert)); |
| } |
| |
| /** Updates the priority list after getting the MANAGE_HEALTH_DATA permission. */ |
| public static void updatePriorityWithManageHealthDataPermission( |
| int permissionCategory, List<String> packageNames) throws InterruptedException { |
| UiAutomation uiAutomation = |
| androidx.test.platform.app.InstrumentationRegistry.getInstrumentation() |
| .getUiAutomation(); |
| |
| uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA); |
| try { |
| updatePriority(permissionCategory, packageNames); |
| } finally { |
| uiAutomation.dropShellPermissionIdentity(); |
| } |
| } |
| |
| /** Updates the priority list without getting the MANAGE_HEALTH_DATA permission. */ |
| public static void updatePriority(int permissionCategory, List<String> packageNames) |
| throws InterruptedException { |
| Context context = ApplicationProvider.getApplicationContext(); |
| HealthConnectManager service = context.getSystemService(HealthConnectManager.class); |
| assertThat(service).isNotNull(); |
| |
| List<DataOrigin> dataOrigins = |
| packageNames.stream() |
| .map( |
| (packageName) -> |
| new DataOrigin.Builder() |
| .setPackageName(packageName) |
| .build()) |
| .collect(Collectors.toList()); |
| |
| CountDownLatch latch = new CountDownLatch(1); |
| AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference = |
| new AtomicReference<>(); |
| UpdateDataOriginPriorityOrderRequest updateDataOriginPriorityOrderRequest = |
| new UpdateDataOriginPriorityOrderRequest(dataOrigins, permissionCategory); |
| service.updateDataOriginPriorityOrder( |
| updateDataOriginPriorityOrderRequest, |
| Executors.newSingleThreadExecutor(), |
| new OutcomeReceiver<>() { |
| @Override |
| public void onResult(Void result) { |
| latch.countDown(); |
| } |
| |
| @Override |
| public void onError(HealthConnectException exception) { |
| healthConnectExceptionAtomicReference.set(exception); |
| latch.countDown(); |
| } |
| }); |
| assertThat(updateDataOriginPriorityOrderRequest.getDataCategory()) |
| .isEqualTo(permissionCategory); |
| assertThat(updateDataOriginPriorityOrderRequest.getDataOriginInOrder()).isNotNull(); |
| assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue(); |
| if (healthConnectExceptionAtomicReference.get() != null) { |
| throw healthConnectExceptionAtomicReference.get(); |
| } |
| } |
| |
| /** Gets the priority list after getting the MANAGE_HEALTH_DATA permission. */ |
| public static FetchDataOriginsPriorityOrderResponse getPriorityWithManageHealthDataPermission( |
| int permissionCategory) throws InterruptedException { |
| UiAutomation uiAutomation = |
| androidx.test.platform.app.InstrumentationRegistry.getInstrumentation() |
| .getUiAutomation(); |
| |
| uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA); |
| FetchDataOriginsPriorityOrderResponse response; |
| |
| try { |
| response = getPriority(permissionCategory); |
| } finally { |
| uiAutomation.dropShellPermissionIdentity(); |
| } |
| |
| return response; |
| } |
| |
| /** Gets the priority list without requesting the MANAGE_HEALTH_DATA permission. */ |
| public static FetchDataOriginsPriorityOrderResponse getPriority(int permissionCategory) |
| throws InterruptedException { |
| Context context = ApplicationProvider.getApplicationContext(); |
| HealthConnectManager service = context.getSystemService(HealthConnectManager.class); |
| assertThat(service).isNotNull(); |
| |
| AtomicReference<FetchDataOriginsPriorityOrderResponse> response = new AtomicReference<>(); |
| CountDownLatch latch = new CountDownLatch(1); |
| AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference = |
| new AtomicReference<>(); |
| service.fetchDataOriginsPriorityOrder( |
| permissionCategory, |
| Executors.newSingleThreadExecutor(), |
| new OutcomeReceiver<>() { |
| @Override |
| public void onResult(FetchDataOriginsPriorityOrderResponse result) { |
| response.set(result); |
| latch.countDown(); |
| } |
| |
| @Override |
| public void onError(HealthConnectException exception) { |
| healthConnectExceptionAtomicReference.set(exception); |
| latch.countDown(); |
| } |
| }); |
| assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue(); |
| if (healthConnectExceptionAtomicReference.get() != null) { |
| throw healthConnectExceptionAtomicReference.get(); |
| } |
| |
| return response.get(); |
| } |
| |
| public static final class RecordAndIdentifier { |
| private final int mId; |
| private final Record mRecordClass; |
| |
| public RecordAndIdentifier(int id, Record recordClass) { |
| this.mId = id; |
| this.mRecordClass = recordClass; |
| } |
| |
| public int getId() { |
| return mId; |
| } |
| |
| public Record getRecordClass() { |
| return mRecordClass; |
| } |
| } |
| |
| public static class RecordTypeInfoTestResponse { |
| private final int mRecordTypePermission; |
| private final ArrayList<String> mContributingPackages; |
| private final int mRecordTypeCategory; |
| |
| RecordTypeInfoTestResponse( |
| int recordTypeCategory, |
| int recordTypePermission, |
| ArrayList<String> contributingPackages) { |
| mRecordTypeCategory = recordTypeCategory; |
| mRecordTypePermission = recordTypePermission; |
| mContributingPackages = contributingPackages; |
| } |
| |
| public int getRecordTypeCategory() { |
| return mRecordTypeCategory; |
| } |
| |
| public int getRecordTypePermission() { |
| return mRecordTypePermission; |
| } |
| |
| public ArrayList<String> getContributingPackages() { |
| return mContributingPackages; |
| } |
| } |
| |
| public static class RecordTypeAndRecordIds implements Serializable { |
| private String mRecordType; |
| private List<String> mRecordIds; |
| |
| public RecordTypeAndRecordIds(String recordType, List<String> ids) { |
| mRecordType = recordType; |
| mRecordIds = ids; |
| } |
| |
| public String getRecordType() { |
| return mRecordType; |
| } |
| |
| public List<String> getRecordIds() { |
| return mRecordIds; |
| } |
| } |
| |
| private static class TestReceiver<T, E extends RuntimeException> |
| implements OutcomeReceiver<T, E> { |
| private final CountDownLatch mLatch = new CountDownLatch(1); |
| private final AtomicReference<T> mResponse = new AtomicReference<>(); |
| private final AtomicReference<E> mException = new AtomicReference<>(); |
| |
| public T getResponse() throws InterruptedException { |
| verifyNoExceptionOrThrow(); |
| return mResponse.get(); |
| } |
| |
| public void verifyNoExceptionOrThrow() throws InterruptedException { |
| assertThat(mLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue(); |
| if (mException.get() != null) { |
| throw mException.get(); |
| } |
| } |
| |
| @Override |
| public void onResult(T result) { |
| mResponse.set(result); |
| mLatch.countDown(); |
| } |
| |
| @Override |
| public void onError(@NonNull E error) { |
| mException.set(error); |
| Log.e(TAG, error.getMessage()); |
| mLatch.countDown(); |
| } |
| } |
| |
| private static final class HealthConnectReceiver<T> |
| extends TestReceiver<T, HealthConnectException> {} |
| |
| private static final class MigrationReceiver extends TestReceiver<Void, MigrationException> {} |
| } |