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.test.app; 18 19 import static android.health.connect.datatypes.StepsRecord.STEPS_COUNT_TOTAL; 20 21 import static java.time.temporal.ChronoUnit.HOURS; 22 import static java.util.Objects.requireNonNull; 23 import static java.util.concurrent.Executors.newSingleThreadExecutor; 24 25 import android.content.BroadcastReceiver; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.health.connect.AggregateRecordsRequest; 29 import android.health.connect.AggregateRecordsResponse; 30 import android.health.connect.HealthConnectException; 31 import android.health.connect.HealthConnectManager; 32 import android.health.connect.InsertRecordsResponse; 33 import android.health.connect.ReadRecordsRequestUsingFilters; 34 import android.health.connect.ReadRecordsRequestUsingIds; 35 import android.health.connect.ReadRecordsResponse; 36 import android.health.connect.TimeInstantRangeFilter; 37 import android.health.connect.changelog.ChangeLogTokenRequest; 38 import android.health.connect.changelog.ChangeLogTokenResponse; 39 import android.health.connect.changelog.ChangeLogsRequest; 40 import android.health.connect.changelog.ChangeLogsResponse; 41 import android.health.connect.datatypes.ActiveCaloriesBurnedRecord; 42 import android.health.connect.datatypes.DataOrigin; 43 import android.health.connect.datatypes.ExerciseSessionRecord; 44 import android.health.connect.datatypes.ExerciseSessionType; 45 import android.health.connect.datatypes.Metadata; 46 import android.health.connect.datatypes.PlannedExerciseSessionRecord; 47 import android.health.connect.datatypes.Record; 48 import android.health.connect.datatypes.StepsRecord; 49 import android.health.connect.datatypes.WeightRecord; 50 import android.health.connect.datatypes.units.Mass; 51 import android.os.Bundle; 52 53 import java.time.Instant; 54 import java.util.ArrayList; 55 import java.util.Arrays; 56 import java.util.List; 57 import java.util.stream.Collectors; 58 59 /** Receives requests from test cases. Required to perform API calls in background. */ 60 public class TestAppReceiver extends BroadcastReceiver { 61 public static final String ACTION_INSERT_STEPS_RECORDS = "action.INSERT_STEPS_RECORDS"; 62 public static final String ACTION_INSERT_WEIGHT_RECORDS = "action.INSERT_WEIGHT_RECORDS"; 63 public static final String ACTION_INSERT_EXERCISE_RECORD = "action.INSERT_EXERCISE_RECORD"; 64 public static final String ACTION_INSERT_PLANNED_EXERCISE_RECORD = 65 "action.INSERT_PLANNED_EXERCISE_RECORD"; 66 public static final String ACTION_READ_STEPS_RECORDS_USING_FILTERS = 67 "action.READ_STEPS_RECORDS_USING_FILTERS"; 68 public static final String ACTION_READ_STEPS_RECORDS_USING_RECORD_IDS = 69 "action.READ_STEPS_RECORDS_USING_RECORD_IDS"; 70 public static final String ACTION_AGGREGATE_STEPS_COUNT = "action.AGGREGATE_STEPS_COUNT"; 71 public static final String ACTION_GET_CHANGE_LOG_TOKEN = "action.GET_CHANGE_LOG_TOKEN"; 72 public static final String ACTION_GET_CHANGE_LOGS = "action.GET_CHANGE_LOGS"; 73 public static final String ACTION_RESULT_SUCCESS = "action.SUCCESS"; 74 public static final String ACTION_RESULT_ERROR = "action.ERROR"; 75 public static final String EXTRA_RESULT_ERROR_CODE = "extra.ERROR_CODE"; 76 public static final String EXTRA_RESULT_ERROR_MESSAGE = "extra.ERROR_MESSAGE"; 77 public static final String EXTRA_RECORD_COUNT = "extra.RECORD_COUNT"; 78 public static final String EXTRA_RECORD_IDS = "extra.RECORD_IDS"; 79 public static final String EXTRA_RECORD_CLIENT_IDS = "extra.RECORD_CLIENT_IDS"; 80 81 /** 82 * This is used to represent either times for InstantRecords or start times for IntervalRecords. 83 */ 84 public static final String EXTRA_TIMES = "extra.TIMES"; 85 86 public static final String EXTRA_END_TIMES = "extra.END_TIMES"; 87 88 /** Represents a list of values in {@code long}. */ 89 public static final String EXTRA_RECORD_VALUES = "extra.RECORD_VALUES"; 90 91 /** Represents a long value. */ 92 public static final String EXTRA_RECORD_VALUE = "extra.RECORD_VALUE"; 93 94 /** This is used to represent the ID of a training plan completed by an exercise. */ 95 public static final String EXTRA_PLANNED_EXERCISE_SESSION_ID = 96 "extra.PLANNED_EXERCISE_SESSION_ID"; 97 98 public static final String EXTRA_TOKEN = "extra.TOKEN"; 99 100 /** Extra for a list of package names. */ 101 public static final String EXTRA_PACKAGE_NAMES = "extra.PACKAGE_NAMES"; 102 103 public static final String EXTRA_SENDER_PACKAGE_NAME = "extra.SENDER_PACKAGE_NAME"; 104 private static final String TEST_SUITE_RECEIVER = 105 "android.healthconnect.cts.utils.TestReceiver"; 106 107 @Override onReceive(Context context, Intent intent)108 public void onReceive(Context context, Intent intent) { 109 switch (intent.getAction()) { 110 case ACTION_INSERT_STEPS_RECORDS: 111 insertStepsRecords(context, intent); 112 break; 113 case ACTION_INSERT_WEIGHT_RECORDS: 114 insertWeightRecords(context, intent); 115 break; 116 case ACTION_INSERT_EXERCISE_RECORD: 117 insertExerciseRecord(context, intent); 118 break; 119 case ACTION_INSERT_PLANNED_EXERCISE_RECORD: 120 insertPlannedExerciseRecord(context, intent); 121 break; 122 case ACTION_READ_STEPS_RECORDS_USING_FILTERS: 123 readStepsRecordsUsingFilters(context, intent); 124 break; 125 case ACTION_READ_STEPS_RECORDS_USING_RECORD_IDS: 126 readStepsRecordsUsingIds(context, intent); 127 break; 128 case ACTION_AGGREGATE_STEPS_COUNT: 129 aggregateStepsCount(context, intent); 130 break; 131 case ACTION_GET_CHANGE_LOG_TOKEN: 132 getChangeLogToken(context, intent); 133 break; 134 case ACTION_GET_CHANGE_LOGS: 135 getChangeLogs(context, intent); 136 break; 137 default: 138 throw new IllegalStateException("Unsupported command: " + intent.getAction()); 139 } 140 } 141 insertStepsRecords(Context context, Intent intent)142 private static void insertStepsRecords(Context context, Intent intent) { 143 DefaultOutcomeReceiver<InsertRecordsResponse> outcome = new DefaultOutcomeReceiver<>(); 144 getHealthConnectManager(context) 145 .insertRecords(createStepsRecords(intent), newSingleThreadExecutor(), outcome); 146 sendInsertRecordsResult(context, intent, outcome); 147 } 148 insertWeightRecords(Context context, Intent intent)149 private static void insertWeightRecords(Context context, Intent intent) { 150 DefaultOutcomeReceiver<InsertRecordsResponse> outcome = new DefaultOutcomeReceiver<>(); 151 getHealthConnectManager(context) 152 .insertRecords(createWeightRecords(intent), newSingleThreadExecutor(), outcome); 153 sendInsertRecordsResult(context, intent, outcome); 154 } 155 insertExerciseRecord(Context context, Intent intent)156 private static void insertExerciseRecord(Context context, Intent intent) { 157 DefaultOutcomeReceiver<InsertRecordsResponse> outcome = new DefaultOutcomeReceiver<>(); 158 getHealthConnectManager(context) 159 .insertRecords( 160 List.of(createExerciseRecord(intent)), newSingleThreadExecutor(), outcome); 161 sendInsertRecordsResult(context, intent, outcome); 162 } 163 insertPlannedExerciseRecord(Context context, Intent intent)164 private static void insertPlannedExerciseRecord(Context context, Intent intent) { 165 DefaultOutcomeReceiver<InsertRecordsResponse> outcome = new DefaultOutcomeReceiver<>(); 166 getHealthConnectManager(context) 167 .insertRecords( 168 List.of(createPlannedExerciseRecord(intent)), 169 newSingleThreadExecutor(), 170 outcome); 171 sendInsertRecordsResult(context, intent, outcome); 172 } 173 readStepsRecordsUsingFilters(Context context, Intent intent)174 private void readStepsRecordsUsingFilters(Context context, Intent intent) { 175 DefaultOutcomeReceiver<ReadRecordsResponse<StepsRecord>> outcome = 176 new DefaultOutcomeReceiver<>(); 177 ReadRecordsRequestUsingFilters.Builder<StepsRecord> requestBuilder = 178 new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class); 179 for (String packageName : getPackageNames(intent)) { 180 requestBuilder.addDataOrigins( 181 new DataOrigin.Builder().setPackageName(packageName).build()); 182 } 183 getHealthConnectManager(context) 184 .readRecords(requestBuilder.build(), newSingleThreadExecutor(), outcome); 185 sendReadRecordsResult(context, intent, outcome); 186 } 187 readStepsRecordsUsingIds(Context context, Intent intent)188 private void readStepsRecordsUsingIds(Context context, Intent intent) { 189 DefaultOutcomeReceiver<ReadRecordsResponse<StepsRecord>> outcome = 190 new DefaultOutcomeReceiver<>(); 191 ReadRecordsRequestUsingIds.Builder<StepsRecord> requestBuilder = 192 new ReadRecordsRequestUsingIds.Builder<>(StepsRecord.class); 193 List<String> recordIds = getRecordIds(intent); 194 for (String recordId : recordIds) { 195 requestBuilder.addId(recordId); 196 } 197 getHealthConnectManager(context) 198 .readRecords(requestBuilder.build(), newSingleThreadExecutor(), outcome); 199 sendReadRecordsResult(context, intent, outcome); 200 } 201 aggregateStepsCount(Context context, Intent intent)202 private void aggregateStepsCount(Context context, Intent intent) { 203 DefaultOutcomeReceiver<AggregateRecordsResponse<Long>> outcome = 204 new DefaultOutcomeReceiver<>(); 205 206 AggregateRecordsRequest.Builder<Long> requestBuilder = 207 new AggregateRecordsRequest.Builder<Long>( 208 new TimeInstantRangeFilter.Builder() 209 .setStartTime(Instant.EPOCH) 210 .setEndTime(Instant.now().plus(10, HOURS)) 211 .build()) 212 .addAggregationType(STEPS_COUNT_TOTAL); 213 for (String packageName : getPackageNames(intent)) { 214 requestBuilder.addDataOriginsFilter( 215 new DataOrigin.Builder().setPackageName(packageName).build()); 216 } 217 getHealthConnectManager(context) 218 .aggregate(requestBuilder.build(), newSingleThreadExecutor(), outcome); 219 220 sendAggregateStepsResult(context, intent, outcome); 221 } 222 getChangeLogToken(Context context, Intent intent)223 private void getChangeLogToken(Context context, Intent intent) { 224 DefaultOutcomeReceiver<ChangeLogTokenResponse> outcome = new DefaultOutcomeReceiver<>(); 225 226 getHealthConnectManager(context) 227 .getChangeLogToken( 228 new ChangeLogTokenRequest.Builder() 229 .addRecordType(ActiveCaloriesBurnedRecord.class) 230 .build(), 231 newSingleThreadExecutor(), 232 outcome); 233 234 final HealthConnectException error = outcome.getError(); 235 if (error == null) { 236 final Bundle extras = new Bundle(); 237 extras.putString(EXTRA_TOKEN, outcome.getResult().getToken()); 238 sendSuccess(context, intent, extras); 239 } else { 240 sendError(context, intent, error); 241 } 242 } 243 getChangeLogs(Context context, Intent intent)244 private void getChangeLogs(Context context, Intent intent) { 245 String token = intent.getStringExtra(EXTRA_TOKEN); 246 DefaultOutcomeReceiver<ChangeLogsResponse> outcome = new DefaultOutcomeReceiver<>(); 247 248 getHealthConnectManager(context) 249 .getChangeLogs( 250 new ChangeLogsRequest.Builder(token).build(), 251 newSingleThreadExecutor(), 252 outcome); 253 254 sendResult(context, intent, outcome); 255 } 256 getHealthConnectManager(Context context)257 private static HealthConnectManager getHealthConnectManager(Context context) { 258 return requireNonNull(context.getSystemService(HealthConnectManager.class)); 259 } 260 sendReadRecordsResult( Context context, Intent intent, DefaultOutcomeReceiver<? extends ReadRecordsResponse<?>> outcome)261 private static void sendReadRecordsResult( 262 Context context, 263 Intent intent, 264 DefaultOutcomeReceiver<? extends ReadRecordsResponse<?>> outcome) { 265 final HealthConnectException error = outcome.getError(); 266 if (error != null) { 267 sendError(context, intent, error); 268 return; 269 } 270 271 final Bundle extras = new Bundle(); 272 List<? extends Record> records = outcome.getResult().getRecords(); 273 extras.putInt(EXTRA_RECORD_COUNT, records.size()); 274 extras.putStringArrayList(EXTRA_RECORD_IDS, new ArrayList<>(getRecordIds(records))); 275 sendSuccess(context, intent, extras); 276 } 277 sendAggregateStepsResult( Context context, Intent intent, DefaultOutcomeReceiver<? extends AggregateRecordsResponse<Long>> outcome)278 private static void sendAggregateStepsResult( 279 Context context, 280 Intent intent, 281 DefaultOutcomeReceiver<? extends AggregateRecordsResponse<Long>> outcome) { 282 final HealthConnectException error = outcome.getError(); 283 if (error != null) { 284 sendError(context, intent, error); 285 return; 286 } 287 288 Bundle extras = new Bundle(); 289 long stepCounts = outcome.getResult().get(STEPS_COUNT_TOTAL); 290 extras.putLong(EXTRA_RECORD_VALUE, stepCounts); 291 sendSuccess(context, intent, extras); 292 } 293 sendInsertRecordsResult( Context context, Intent intent, DefaultOutcomeReceiver<? extends InsertRecordsResponse> outcome)294 private static void sendInsertRecordsResult( 295 Context context, 296 Intent intent, 297 DefaultOutcomeReceiver<? extends InsertRecordsResponse> outcome) { 298 final HealthConnectException error = outcome.getError(); 299 if (error != null) { 300 sendError(context, intent, error); 301 return; 302 } 303 304 final Bundle extras = new Bundle(); 305 List<? extends Record> records = outcome.getResult().getRecords(); 306 ArrayList<String> recordIds = 307 new ArrayList<>( 308 records.stream() 309 .map(Record::getMetadata) 310 .map(Metadata::getId) 311 .collect(Collectors.toList())); 312 extras.putStringArrayList(EXTRA_RECORD_IDS, recordIds); 313 extras.putInt(EXTRA_RECORD_COUNT, records.size()); 314 sendSuccess(context, intent, extras); 315 } 316 sendResult( Context context, Intent intent, DefaultOutcomeReceiver<?> outcomeReceiver)317 private static void sendResult( 318 Context context, Intent intent, DefaultOutcomeReceiver<?> outcomeReceiver) { 319 final HealthConnectException error = outcomeReceiver.getError(); 320 if (error != null) { 321 sendError(context, intent, error); 322 return; 323 } 324 sendSuccess(context, intent); 325 } 326 sendSuccess(Context context, Intent intent)327 private static void sendSuccess(Context context, Intent intent) { 328 context.sendBroadcast(getSuccessIntent(intent)); 329 } 330 sendSuccess(Context context, Intent intent, Bundle extras)331 private static void sendSuccess(Context context, Intent intent, Bundle extras) { 332 context.sendBroadcast(getSuccessIntent(intent).putExtras(extras)); 333 } 334 getSuccessIntent(Intent intent)335 private static Intent getSuccessIntent(Intent intent) { 336 return new Intent(ACTION_RESULT_SUCCESS) 337 .setClassName(getSenderPackageName(intent), TEST_SUITE_RECEIVER); 338 } 339 sendError(Context context, Intent intent, HealthConnectException error)340 private static void sendError(Context context, Intent intent, HealthConnectException error) { 341 context.sendBroadcast( 342 new Intent(ACTION_RESULT_ERROR) 343 .setClassName(getSenderPackageName(intent), TEST_SUITE_RECEIVER) 344 .putExtra(EXTRA_RESULT_ERROR_CODE, error.getErrorCode()) 345 .putExtra(EXTRA_RESULT_ERROR_MESSAGE, error.getMessage())); 346 } 347 createStepsRecords(Intent intent)348 private static List<Record> createStepsRecords(Intent intent) { 349 List<Instant> startTimes = getTimes(intent, EXTRA_TIMES); 350 List<Instant> endTimes = getTimes(intent, EXTRA_END_TIMES); 351 String[] clientIds = intent.getStringArrayExtra(EXTRA_RECORD_CLIENT_IDS); 352 long[] values = intent.getLongArrayExtra(EXTRA_RECORD_VALUES); 353 354 List<Record> result = new ArrayList<>(); 355 for (int i = 0; i < startTimes.size(); i++) { 356 result.add( 357 createStepsRecord(startTimes.get(i), endTimes.get(i), clientIds[i], values[i])); 358 } 359 return result; 360 } 361 createStepsRecord( Instant startTime, Instant endTime, String clientId, long steps)362 private static StepsRecord createStepsRecord( 363 Instant startTime, Instant endTime, String clientId, long steps) { 364 Metadata.Builder metadataBuilder = new Metadata.Builder(); 365 metadataBuilder.setClientRecordId(clientId); 366 return new StepsRecord.Builder(metadataBuilder.build(), startTime, endTime, steps).build(); 367 } 368 createWeightRecords(Intent intent)369 private static List<Record> createWeightRecords(Intent intent) { 370 List<Instant> times = getTimes(intent, EXTRA_TIMES); 371 String[] clientIds = intent.getStringArrayExtra(EXTRA_RECORD_CLIENT_IDS); 372 double[] values = intent.getDoubleArrayExtra(EXTRA_RECORD_VALUES); 373 374 List<Record> result = new ArrayList<>(); 375 for (int i = 0; i < times.size(); i++) { 376 result.add(createWeightRecord(times.get(i), clientIds[i], values[i])); 377 } 378 return result; 379 } 380 createExerciseRecord(Intent intent)381 private static Record createExerciseRecord(Intent intent) { 382 String trainingPlanId = intent.getStringExtra(EXTRA_PLANNED_EXERCISE_SESSION_ID); 383 ExerciseSessionRecord record = 384 new ExerciseSessionRecord.Builder( 385 new Metadata.Builder().build(), 386 getTimes(intent, EXTRA_TIMES).get(0), 387 getTimes(intent, EXTRA_END_TIMES).get(0), 388 ExerciseSessionType.EXERCISE_SESSION_TYPE_BIKING) 389 .setPlannedExerciseSessionId(trainingPlanId) 390 .build(); 391 return record; 392 } 393 createPlannedExerciseRecord(Intent intent)394 private static Record createPlannedExerciseRecord(Intent intent) { 395 PlannedExerciseSessionRecord record = 396 new PlannedExerciseSessionRecord.Builder( 397 new Metadata.Builder().build(), 398 ExerciseSessionType.EXERCISE_SESSION_TYPE_BIKING, 399 getTimes(intent, EXTRA_TIMES).get(0), 400 getTimes(intent, EXTRA_END_TIMES).get(0)) 401 .build(); 402 return record; 403 } 404 createWeightRecord(Instant time, String clientId, double weight)405 private static WeightRecord createWeightRecord(Instant time, String clientId, double weight) { 406 return new WeightRecord.Builder( 407 new Metadata.Builder().setClientRecordId(clientId).build(), 408 time, 409 Mass.fromGrams(weight)) 410 .build(); 411 } 412 getTimes(Intent intent, String key)413 private static List<Instant> getTimes(Intent intent, String key) { 414 return Arrays.stream(intent.getLongArrayExtra(key)) 415 .mapToObj(Instant::ofEpochMilli) 416 .collect(Collectors.toList()); 417 } 418 getPackageNames(Intent intent)419 private static List<String> getPackageNames(Intent intent) { 420 List<String> packageNames = intent.getStringArrayListExtra(EXTRA_PACKAGE_NAMES); 421 return packageNames == null ? new ArrayList<>() : packageNames; 422 } 423 getRecordIds(Intent intent)424 private static List<String> getRecordIds(Intent intent) { 425 List<String> recordIds = intent.getStringArrayListExtra(EXTRA_RECORD_IDS); 426 return recordIds == null ? new ArrayList<>() : recordIds; 427 } 428 getSenderPackageName(Intent intent)429 private static String getSenderPackageName(Intent intent) { 430 return intent.getStringExtra(EXTRA_SENDER_PACKAGE_NAME); 431 } 432 getRecordIds(List<? extends Record> records)433 private static List<String> getRecordIds(List<? extends Record> records) { 434 return records.stream().map(Record::getMetadata).map(Metadata::getId).toList(); 435 } 436 } 437