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.lib; 18 19 import static java.util.Objects.requireNonNull; 20 21 import android.health.connect.AggregateRecordsRequest; 22 import android.health.connect.AggregateRecordsResponse; 23 import android.health.connect.CreateMedicalDataSourceRequest; 24 import android.health.connect.DeleteMedicalResourcesRequest; 25 import android.health.connect.GetMedicalDataSourcesRequest; 26 import android.health.connect.MedicalResourceId; 27 import android.health.connect.ReadMedicalResourcesInitialRequest; 28 import android.health.connect.ReadMedicalResourcesPageRequest; 29 import android.health.connect.ReadMedicalResourcesRequest; 30 import android.health.connect.ReadMedicalResourcesResponse; 31 import android.health.connect.ReadRecordsRequestUsingFilters; 32 import android.health.connect.ReadRecordsRequestUsingIds; 33 import android.health.connect.RecordIdFilter; 34 import android.health.connect.TimeInstantRangeFilter; 35 import android.health.connect.UpsertMedicalResourceRequest; 36 import android.health.connect.changelog.ChangeLogTokenRequest; 37 import android.health.connect.changelog.ChangeLogsRequest; 38 import android.health.connect.changelog.ChangeLogsResponse; 39 import android.health.connect.datatypes.BasalMetabolicRateRecord; 40 import android.health.connect.datatypes.DataOrigin; 41 import android.health.connect.datatypes.Device; 42 import android.health.connect.datatypes.DistanceRecord; 43 import android.health.connect.datatypes.ExerciseLap; 44 import android.health.connect.datatypes.ExerciseRoute; 45 import android.health.connect.datatypes.ExerciseSegment; 46 import android.health.connect.datatypes.ExerciseSessionRecord; 47 import android.health.connect.datatypes.HeartRateRecord; 48 import android.health.connect.datatypes.InstantRecord; 49 import android.health.connect.datatypes.IntervalRecord; 50 import android.health.connect.datatypes.MedicalDataSource; 51 import android.health.connect.datatypes.MedicalResource; 52 import android.health.connect.datatypes.MenstruationPeriodRecord; 53 import android.health.connect.datatypes.Metadata; 54 import android.health.connect.datatypes.NutritionRecord; 55 import android.health.connect.datatypes.PlannedExerciseSessionRecord; 56 import android.health.connect.datatypes.Record; 57 import android.health.connect.datatypes.SleepSessionRecord; 58 import android.health.connect.datatypes.SleepSessionRecord.Stage; 59 import android.health.connect.datatypes.StepsRecord; 60 import android.health.connect.datatypes.TotalCaloriesBurnedRecord; 61 import android.health.connect.datatypes.WeightRecord; 62 import android.health.connect.datatypes.units.Energy; 63 import android.health.connect.datatypes.units.Length; 64 import android.health.connect.datatypes.units.Mass; 65 import android.health.connect.datatypes.units.Power; 66 import android.healthconnect.cts.utils.ToStringUtils; 67 import android.os.Bundle; 68 import android.util.Log; 69 70 import java.lang.reflect.InvocationTargetException; 71 import java.time.Instant; 72 import java.time.ZoneOffset; 73 import java.util.ArrayList; 74 import java.util.HashSet; 75 import java.util.List; 76 import java.util.Objects; 77 import java.util.Set; 78 import java.util.function.Consumer; 79 import java.util.function.Function; 80 import java.util.stream.IntStream; 81 82 /** Converters from/to bundles for HC request, response, and record types. */ 83 public final class BundleHelper { 84 private static final String TAG = "TestApp-BundleHelper"; 85 static final String PREFIX = "android.healthconnect.cts."; 86 public static final String QUERY_TYPE = PREFIX + "QUERY_TYPE"; 87 public static final String INSERT_RECORDS_QUERY = PREFIX + "INSERT_RECORDS_QUERY"; 88 public static final String READ_RECORDS_QUERY = PREFIX + "READ_RECORDS_QUERY"; 89 public static final String READ_RECORDS_USING_IDS_QUERY = 90 PREFIX + "READ_RECORDS_USING_IDS_QUERY"; 91 public static final String AGGREGATE_STEPS_COUNT_TOTAL_QUERY = 92 PREFIX + "AGGREGATE_STEPS_COUNT_TOTAL_QUERY"; 93 public static final String READ_CHANGE_LOGS_QUERY = PREFIX + "READ_CHANGE_LOGS_QUERY"; 94 public static final String DELETE_RECORDS_QUERY = PREFIX + "DELETE_RECORDS_QUERY"; 95 public static final String UPDATE_RECORDS_QUERY = PREFIX + "UPDATE_RECORDS_QUERY"; 96 public static final String GET_CHANGE_LOG_TOKEN_QUERY = PREFIX + "GET_CHANGE_LOG_TOKEN_QUERY"; 97 public static final String CREATE_MEDICAL_DATA_SOURCE_QUERY = 98 PREFIX + "CREATE_MEDICAL_DATA_SOURCE_QUERY"; 99 public static final String GET_MEDICAL_DATA_SOURCES_USING_IDS_QUERY = 100 PREFIX + "GET_MEDICAL_DATA_SOURCES_USING_IDS_QUERY"; 101 public static final String GET_MEDICAL_DATA_SOURCES_USING_REQUEST_QUERY = 102 PREFIX + "GET_MEDICAL_DATA_SOURCES_USING_REQUEST_QUERY"; 103 public static final String UPSERT_MEDICAL_RESOURCES_QUERY = 104 PREFIX + "UPSERT_MEDICAL_RESOURCE_QUERY"; 105 public static final String READ_MEDICAL_RESOURCES_BY_REQUEST_QUERY = 106 PREFIX + "READ_MEDICAL_RESOURCES_BY_REQUEST_QUERY"; 107 public static final String READ_MEDICAL_RESOURCES_BY_IDS_QUERY = 108 PREFIX + "READ_MEDICAL_RESOURCES_BY_IDS_QUERY"; 109 public static final String DELETE_MEDICAL_RESOURCES_BY_REQUEST_QUERY = 110 PREFIX + "DELETE_MEDICAL_RESOURCES_BY_REQUEST_QUERY"; 111 public static final String DELETE_MEDICAL_RESOURCES_BY_IDS_QUERY = 112 PREFIX + "DELETE_MEDICAL_RESOURCES_BY_IDS_QUERY"; 113 public static final String DELETE_MEDICAL_DATA_SOURCE_WITH_DATA_QUERY = 114 PREFIX + "DELETE_MEDICAL_DATA_SOURCE_WITH_DATA_QUERY"; 115 116 private static final String CREATE_MEDICAL_DATA_SOURCE_REQUEST = 117 PREFIX + "CREATE_MEDICAL_DATA_SOURCE_REQUEST"; 118 private static final String GET_MEDICAL_DATA_SOURCES_REQUEST = 119 PREFIX + "GET_MEDICAL_DATA_SOURCES_REQUEST"; 120 public static final String MEDICAL_DATA_SOURCE_RESPONSE = 121 PREFIX + "MEDICAL_DATA_SOURCE_RESPONSE"; 122 public static final String MEDICAL_DATA_SOURCES_RESPONSE = 123 PREFIX + "MEDICAL_DATA_SOURCE_RESPONSE"; 124 private static final String UPSERT_MEDICAL_RESOURCE_REQUESTS = 125 PREFIX + "UPSERT_MEDICAL_RESOURCE_REQUEST"; 126 private static final String READ_MEDICAL_RESOURCES_REQUEST_IS_PAGE_REQUEST = 127 PREFIX + "READ_MEDICAL_RESOURCES_REQUEST_IS_PAGE_REQUEST"; 128 private static final String READ_MEDICAL_RESOURCES_REQUEST_MEDICAL_RESOURCE_TYPE = 129 PREFIX + "READ_MEDICAL_RESOURCES_REQUEST_MEDICAL_RESOURCE_TYPE"; 130 private static final String READ_MEDICAL_RESOURCES_REQUEST_DATA_SOURCE_IDS = 131 PREFIX + "READ_MEDICAL_RESOURCES_REQUEST_DATA_SOURCE_IDS"; 132 private static final String READ_MEDICAL_RESOURCES_REQUEST_PAGE_TOKEN = 133 PREFIX + "READ_MEDICAL_RESOURCES_REQUEST_PAGE_TOKEN"; 134 private static final String READ_MEDICAL_RESOURCES_REQUEST_PAGE_SIZE = 135 PREFIX + "READ_MEDICAL_RESOURCES_REQUEST_PAGE_SIZE"; 136 private static final String MEDICAL_RESOURCE_IDS = PREFIX + "MEDICAL_RESOURCE_IDS"; 137 public static final String MEDICAL_RESOURCES_RESPONSE = PREFIX + "MEDICAL_RESOURCE_RESPONSE"; 138 public static final String READ_MEDICAL_RESOURCES_RESPONSE = 139 PREFIX + "READ_MEDICAL_RESOURCES_RESPONSE"; 140 private static final String DELETE_MEDICAL_RESOURCES_REQUEST = 141 PREFIX + "DELETE_MEDICAL_RESOURCES_REQUEST"; 142 143 public static final String SELF_REVOKE_PERMISSION_REQUEST = 144 PREFIX + "SELF_REVOKE_PERMISSION_REQUEST"; 145 146 public static final String KILL_SELF_REQUEST = PREFIX + "KILL_SELF_REQUEST"; 147 148 public static final String INTENT_EXCEPTION = PREFIX + "INTENT_EXCEPTION"; 149 150 private static final String CHANGE_LOGS_RESPONSE = PREFIX + "CHANGE_LOGS_RESPONSE"; 151 private static final String CHANGE_LOG_TOKEN = PREFIX + "CHANGE_LOG_TOKEN"; 152 private static final String RECORD_CLASS_NAME = PREFIX + "RECORD_CLASS_NAME"; 153 private static final String START_TIME_MILLIS = PREFIX + "START_TIME_MILLIS"; 154 private static final String END_TIME_MILLIS = PREFIX + "END_TIME_MILLIS"; 155 private static final String EXERCISE_SESSION_TYPE = PREFIX + "EXERCISE_SESSION_TYPE"; 156 private static final String RECORD_LIST = PREFIX + "RECORD_LIST"; 157 private static final String PACKAGE_NAME = PREFIX + "PACKAGE_NAME"; 158 private static final String CLIENT_ID = PREFIX + "CLIENT_ID"; 159 private static final String RECORD_ID = PREFIX + "RECORD_ID"; 160 private static final String AGGREGATE_STEPS_COUNT_TOTAL_RESULT = 161 PREFIX + "AGGREGATE_STEPS_COUNT_TOTAL_RESULT"; 162 private static final String MEDICAL_DATA_SOURCE_ID = PREFIX + "MEDICAL_DATA_SOURCE_ID"; 163 private static final String METADATA = PREFIX + "METADATA"; 164 private static final String DEVICE = PREFIX + "DEVICE"; 165 private static final String DEVICE_TYPE = PREFIX + "DEVICE_TYPE"; 166 private static final String MANUFACTURER = PREFIX + "MANUFACTURER"; 167 private static final String MODEL = PREFIX + "MODEL"; 168 private static final String VALUES = PREFIX + "VALUES"; 169 private static final String COUNT = PREFIX + "COUNT"; 170 private static final String LENGTH_IN_METERS = PREFIX + "LENGTH_IN_METERS"; 171 private static final String ENERGY_IN_CALORIES = PREFIX + "ENERGY_IN_CALORIES"; 172 private static final String WEIGHT_IN_GRAMS = PREFIX + "WEIGHT_IN_GRAMS"; 173 private static final String IRON_IN_GRAMS = PREFIX + "IRON_IN_GRAMS"; 174 private static final String SAMPLE_TIMES = PREFIX + "SAMPLE_TIMES"; 175 private static final String SAMPLE_VALUES = PREFIX + "SAMPLE_VALUES"; 176 private static final String EXERCISE_ROUTE_TIMESTAMPS = PREFIX + "EXERCISE_ROUTE_TIMESTAMPS"; 177 private static final String EXERCISE_ROUTE_LATITUDES = PREFIX + "EXERCISE_ROUTE_LATITUDES"; 178 private static final String EXERCISE_ROUTE_LONGITUDES = PREFIX + "EXERCISE_ROUTE_LONGITUDES"; 179 private static final String EXERCISE_ROUTE_ALTITUDES = PREFIX + "EXERCISE_ROUTE_ALTITUDES"; 180 private static final String EXERCISE_ROUTE_HACCS = PREFIX + "EXERCISE_ROUTE_HACCS"; 181 private static final String EXERCISE_ROUTE_VACCS = PREFIX + "EXERCISE_ROUTE_VACCS"; 182 private static final String EXERCISE_HAS_ROUTE = PREFIX + "EXERCISE_HAS_ROUTE"; 183 private static final String EXERCISE_LAPS = PREFIX + "EXERCISE_LAPS"; 184 private static final String POWER_WATTS = PREFIX + "POWER_WATTS"; 185 private static final String TIME_INSTANT_RANGE_FILTER = PREFIX + "TIME_INSTANT_RANGE_FILTER"; 186 private static final String CHANGE_LOGS_REQUEST = PREFIX + "CHANGE_LOGS_REQUEST"; 187 private static final String CHANGE_LOG_TOKEN_REQUEST = PREFIX + "CHANGE_LOG_TOKEN_REQUEST"; 188 private static final String PERMISSION_NAME = PREFIX + "PERMISSION_NAME"; 189 private static final String START_TIMES = PREFIX + "START_TIMES"; 190 private static final String END_TIMES = PREFIX + "END_TIMES"; 191 private static final String EXERCISE_SEGMENT_TYPES = PREFIX + "EXERCISE_SEGMENT_TYPES"; 192 private static final String EXERCISE_SEGMENT_REP_COUNTS = 193 PREFIX + "EXERCISE_SEGMENT_REP_COUNTS"; 194 private static final String NOTES = PREFIX + "NOTES"; 195 private static final String TITLE = PREFIX + "TITLE"; 196 private static final String PLANNED_EXERCISE_SESSION_ID = 197 PREFIX + "PLANNED_EXERCISE_SESSION_ID"; 198 private static final String START_ZONE_OFFSET = PREFIX + "START_ZONE_OFFSET"; 199 private static final String END_ZONE_OFFSET = PREFIX + "END_ZONE_OFFSET"; 200 201 /** Converts an insert records request to a bundle. */ fromInsertRecordsRequest(List<? extends Record> records)202 public static Bundle fromInsertRecordsRequest(List<? extends Record> records) { 203 Bundle bundle = new Bundle(); 204 bundle.putString(QUERY_TYPE, INSERT_RECORDS_QUERY); 205 bundle.putParcelableArrayList(RECORD_LIST, new ArrayList<>(fromRecordList(records))); 206 return bundle; 207 } 208 209 /** Converts a bundle to an insert records request. */ toInsertRecordsRequest(Bundle bundle)210 public static List<? extends Record> toInsertRecordsRequest(Bundle bundle) { 211 return toRecordList(bundle.getParcelableArrayList(RECORD_LIST, Bundle.class)); 212 } 213 214 /** Converts an update records request to a bundle. */ fromUpdateRecordsRequest(List<Record> records)215 public static Bundle fromUpdateRecordsRequest(List<Record> records) { 216 Bundle bundle = new Bundle(); 217 bundle.putString(QUERY_TYPE, UPDATE_RECORDS_QUERY); 218 bundle.putParcelableArrayList(RECORD_LIST, new ArrayList<>(fromRecordList(records))); 219 return bundle; 220 } 221 222 /** Converts a bundle to an update records request. */ toUpdateRecordsRequest(Bundle bundle)223 public static List<? extends Record> toUpdateRecordsRequest(Bundle bundle) { 224 return toRecordList(bundle.getParcelableArrayList(RECORD_LIST, Bundle.class)); 225 } 226 227 /** Converts an insert records response to a bundle. */ fromInsertRecordsResponse(List<String> recordIds)228 public static Bundle fromInsertRecordsResponse(List<String> recordIds) { 229 Bundle bundle = new Bundle(); 230 bundle.putStringArrayList(RECORD_ID, new ArrayList<>(recordIds)); 231 return bundle; 232 } 233 234 /** Converts a bundle to an insert records response. */ toInsertRecordsResponse(Bundle bundle)235 public static List<String> toInsertRecordsResponse(Bundle bundle) { 236 return bundle.getStringArrayList(RECORD_ID); 237 } 238 239 /** Converts a ReadRecordsRequestUsingFilters to a bundle. */ fromReadRecordsRequestUsingFilters( ReadRecordsRequestUsingFilters<T> request)240 public static <T extends Record> Bundle fromReadRecordsRequestUsingFilters( 241 ReadRecordsRequestUsingFilters<T> request) { 242 Bundle bundle = new Bundle(); 243 bundle.putString(QUERY_TYPE, READ_RECORDS_QUERY); 244 bundle.putString(RECORD_CLASS_NAME, request.getRecordType().getName()); 245 bundle.putStringArrayList( 246 PACKAGE_NAME, 247 new ArrayList<>( 248 request.getDataOrigins().stream() 249 .map(DataOrigin::getPackageName) 250 .toList())); 251 252 if (request.getTimeRangeFilter() instanceof TimeInstantRangeFilter filter) { 253 bundle.putBoolean(TIME_INSTANT_RANGE_FILTER, true); 254 255 Long startTime = transformOrNull(filter.getStartTime(), Instant::toEpochMilli); 256 Long endTime = transformOrNull(filter.getEndTime(), Instant::toEpochMilli); 257 258 bundle.putSerializable(START_TIME_MILLIS, startTime); 259 bundle.putSerializable(END_TIME_MILLIS, endTime); 260 } else if (request.getTimeRangeFilter() != null) { 261 throw new IllegalArgumentException("Unsupported time range filter"); 262 } 263 264 return bundle; 265 } 266 267 /** Converts a bundle to a ReadRecordsRequestUsingFilters. */ toReadRecordsRequestUsingFilters( Bundle bundle)268 public static ReadRecordsRequestUsingFilters<? extends Record> toReadRecordsRequestUsingFilters( 269 Bundle bundle) { 270 String recordClassName = bundle.getString(RECORD_CLASS_NAME); 271 272 Class<? extends Record> recordClass = recordClassForName(recordClassName); 273 274 ReadRecordsRequestUsingFilters.Builder<? extends Record> request = 275 new ReadRecordsRequestUsingFilters.Builder<>(recordClass); 276 277 if (bundle.getBoolean(TIME_INSTANT_RANGE_FILTER)) { 278 Long startTimeMillis = bundle.getSerializable(START_TIME_MILLIS, Long.class); 279 Long endTimeMillis = bundle.getSerializable(END_TIME_MILLIS, Long.class); 280 281 Instant startTime = transformOrNull(startTimeMillis, Instant::ofEpochMilli); 282 Instant endTime = transformOrNull(endTimeMillis, Instant::ofEpochMilli); 283 284 TimeInstantRangeFilter timeInstantRangeFilter = 285 new TimeInstantRangeFilter.Builder() 286 .setStartTime(startTime) 287 .setEndTime(endTime) 288 .build(); 289 290 request.setTimeRangeFilter(timeInstantRangeFilter); 291 } 292 List<String> packageNames = bundle.getStringArrayList(PACKAGE_NAME); 293 294 if (packageNames != null) { 295 for (String packageName : packageNames) { 296 request.addDataOrigins( 297 new DataOrigin.Builder().setPackageName(packageName).build()); 298 } 299 } 300 301 return request.build(); 302 } 303 304 /** Converts a ReadRecordsRequestUsingFilters to a bundle. */ fromReadRecordsRequestUsingIds( ReadRecordsRequestUsingIds<T> request)305 public static <T extends Record> Bundle fromReadRecordsRequestUsingIds( 306 ReadRecordsRequestUsingIds<T> request) { 307 Bundle bundle = new Bundle(); 308 bundle.putString(QUERY_TYPE, READ_RECORDS_USING_IDS_QUERY); 309 bundle.putString(RECORD_CLASS_NAME, request.getRecordType().getName()); 310 311 var recordIdFilters = request.getRecordIdFilters(); 312 bundle.putStringArrayList( 313 RECORD_ID, 314 new ArrayList<>( 315 recordIdFilters.stream() 316 .map(RecordIdFilter::getId) 317 .filter(Objects::nonNull) 318 .toList())); 319 bundle.putStringArrayList( 320 CLIENT_ID, 321 new ArrayList<>( 322 recordIdFilters.stream() 323 .map(RecordIdFilter::getClientRecordId) 324 .filter(Objects::nonNull) 325 .toList())); 326 327 return bundle; 328 } 329 330 /** Converts a bundle to a ReadRecordsRequestUsingFilters. */ toReadRecordsRequestUsingIds( Bundle bundle)331 public static ReadRecordsRequestUsingIds<? extends Record> toReadRecordsRequestUsingIds( 332 Bundle bundle) { 333 String recordClassName = bundle.getString(RECORD_CLASS_NAME); 334 var request = new ReadRecordsRequestUsingIds.Builder<>(recordClassForName(recordClassName)); 335 var recordIds = bundle.getStringArrayList(RECORD_ID); 336 if (recordIds != null) { 337 for (String id : recordIds) { 338 request.addId(id); 339 } 340 } 341 var clientRecordIds = bundle.getStringArrayList(CLIENT_ID); 342 if (clientRecordIds != null) { 343 for (String clientId : clientRecordIds) { 344 request.addClientRecordId(clientId); 345 } 346 } 347 348 return request.build(); 349 } 350 351 /** Converts a read records response to a bundle. */ fromReadRecordsResponse(List<? extends Record> records)352 public static Bundle fromReadRecordsResponse(List<? extends Record> records) { 353 Bundle bundle = new Bundle(); 354 bundle.putParcelableArrayList(RECORD_LIST, new ArrayList<>(fromRecordList(records))); 355 return bundle; 356 } 357 358 /** Converts a bundle to a read records response. */ toReadRecordsResponse(Bundle bundle)359 public static <T extends Record> List<T> toReadRecordsResponse(Bundle bundle) { 360 return (List<T>) toRecordList(bundle.getParcelableArrayList(RECORD_LIST, Bundle.class)); 361 } 362 363 /** Converts an aggregate steps count total request to a bundle. */ fromAggregateStepsCountTotalRequest( Instant startTime, Instant endTime, List<String> packageNames)364 public static Bundle fromAggregateStepsCountTotalRequest( 365 Instant startTime, Instant endTime, List<String> packageNames) { 366 Bundle bundle = new Bundle(); 367 bundle.putString(QUERY_TYPE, AGGREGATE_STEPS_COUNT_TOTAL_QUERY); 368 bundle.putLong(START_TIME_MILLIS, startTime.toEpochMilli()); 369 bundle.putLong(END_TIME_MILLIS, endTime.toEpochMilli()); 370 bundle.putStringArrayList(PACKAGE_NAME, new ArrayList<>(packageNames)); 371 return bundle; 372 } 373 374 /** Converts a bundle to an aggregate steps count total request. */ toAggregateStepsCountTotalRequest(Bundle bundle)375 public static AggregateRecordsRequest<Long> toAggregateStepsCountTotalRequest(Bundle bundle) { 376 Instant startTime = Instant.ofEpochMilli(bundle.getLong(START_TIME_MILLIS)); 377 Instant endTime = Instant.ofEpochMilli(bundle.getLong(END_TIME_MILLIS)); 378 TimeInstantRangeFilter timeInstantRangeFilter = 379 new TimeInstantRangeFilter.Builder() 380 .setStartTime(startTime) 381 .setEndTime(endTime) 382 .build(); 383 384 AggregateRecordsRequest.Builder<Long> request = 385 new AggregateRecordsRequest.Builder<Long>(timeInstantRangeFilter) 386 .addAggregationType(StepsRecord.STEPS_COUNT_TOTAL); 387 388 List<String> packageNames = requireNonNull(bundle.getStringArrayList(PACKAGE_NAME)); 389 for (String packageName : packageNames) { 390 request.addDataOriginsFilter( 391 new DataOrigin.Builder().setPackageName(packageName).build()); 392 } 393 394 return request.build(); 395 } 396 397 /** Converts an aggregate steps count total response to a bundle. */ fromAggregateStepsCountTotalResponse( AggregateRecordsResponse<Long> response)398 public static Bundle fromAggregateStepsCountTotalResponse( 399 AggregateRecordsResponse<Long> response) { 400 Bundle bundle = new Bundle(); 401 Long result = response.get(StepsRecord.STEPS_COUNT_TOTAL); 402 if (result != null) { 403 bundle.putLong(AGGREGATE_STEPS_COUNT_TOTAL_RESULT, result); 404 } 405 return bundle; 406 } 407 408 /** Converts a bundle to an aggregate steps count total response. */ toAggregateStepsCountTotalResponse(Bundle bundle)409 public static Long toAggregateStepsCountTotalResponse(Bundle bundle) { 410 return bundle.containsKey(AGGREGATE_STEPS_COUNT_TOTAL_RESULT) 411 ? bundle.getLong(AGGREGATE_STEPS_COUNT_TOTAL_RESULT) 412 : null; 413 } 414 415 /** Converts a delete records request to a bundle. */ fromDeleteRecordsByIdsRequest(List<RecordIdFilter> recordIdFilters)416 public static Bundle fromDeleteRecordsByIdsRequest(List<RecordIdFilter> recordIdFilters) { 417 Bundle bundle = new Bundle(); 418 bundle.putString(QUERY_TYPE, DELETE_RECORDS_QUERY); 419 420 List<String> recordClassNames = 421 recordIdFilters.stream() 422 .map(RecordIdFilter::getRecordType) 423 .map(Class::getName) 424 .toList(); 425 List<String> recordIds = recordIdFilters.stream().map(RecordIdFilter::getId).toList(); 426 427 bundle.putStringArrayList(RECORD_CLASS_NAME, new ArrayList<>(recordClassNames)); 428 bundle.putStringArrayList(RECORD_ID, new ArrayList<>(recordIds)); 429 430 return bundle; 431 } 432 433 /** Converts a bundle to a delete records request. */ toDeleteRecordsByIdsRequest(Bundle bundle)434 public static List<RecordIdFilter> toDeleteRecordsByIdsRequest(Bundle bundle) { 435 List<String> recordClassNames = bundle.getStringArrayList(RECORD_CLASS_NAME); 436 List<String> recordIds = bundle.getStringArrayList(RECORD_ID); 437 438 return IntStream.range(0, recordClassNames.size()) 439 .mapToObj( 440 i -> { 441 String recordClassName = recordClassNames.get(i); 442 Class<? extends Record> recordClass = 443 recordClassForName(recordClassName); 444 String recordId = recordIds.get(i); 445 return RecordIdFilter.fromId(recordClass, recordId); 446 }) 447 .toList(); 448 } 449 450 /** Converts a ChangeLogTokenRequest to a bundle. */ fromChangeLogTokenRequest(ChangeLogTokenRequest request)451 public static Bundle fromChangeLogTokenRequest(ChangeLogTokenRequest request) { 452 Bundle bundle = new Bundle(); 453 bundle.putString(QUERY_TYPE, GET_CHANGE_LOG_TOKEN_QUERY); 454 bundle.putParcelable(CHANGE_LOG_TOKEN_REQUEST, request); 455 return bundle; 456 } 457 458 /** Converts a self-revoke permission request to a bundle. */ forSelfRevokePermissionRequest(String permission)459 public static Bundle forSelfRevokePermissionRequest(String permission) { 460 Bundle bundle = new Bundle(); 461 bundle.putString(QUERY_TYPE, SELF_REVOKE_PERMISSION_REQUEST); 462 bundle.putString(PERMISSION_NAME, permission); 463 return bundle; 464 } 465 466 /** Creates a bundle representing a kill-self request. */ forKillSelfRequest()467 public static Bundle forKillSelfRequest() { 468 Bundle bundle = new Bundle(); 469 bundle.putString(QUERY_TYPE, KILL_SELF_REQUEST); 470 return bundle; 471 } 472 473 /** Converts a bundle to a self-revoke permission request. */ toPermissionToSelfRevoke(Bundle bundle)474 public static String toPermissionToSelfRevoke(Bundle bundle) { 475 return bundle.getString(PERMISSION_NAME); 476 } 477 478 /** Converts a bundle to a ChangeLogTokenRequest. */ toChangeLogTokenRequest(Bundle bundle)479 public static ChangeLogTokenRequest toChangeLogTokenRequest(Bundle bundle) { 480 return bundle.getParcelable(CHANGE_LOG_TOKEN_REQUEST, ChangeLogTokenRequest.class); 481 } 482 483 /** Converts a changelog token response to a bundle. */ fromChangeLogTokenResponse(String token)484 public static Bundle fromChangeLogTokenResponse(String token) { 485 Bundle bundle = new Bundle(); 486 bundle.putString(CHANGE_LOG_TOKEN, token); 487 return bundle; 488 } 489 490 /** Converts a bundle to a change log token response. */ toChangeLogTokenResponse(Bundle bundle)491 public static String toChangeLogTokenResponse(Bundle bundle) { 492 return bundle.getString(CHANGE_LOG_TOKEN); 493 } 494 495 /** Converts a ChangeLogsRequest to a bundle. */ fromChangeLogsRequest(ChangeLogsRequest request)496 public static Bundle fromChangeLogsRequest(ChangeLogsRequest request) { 497 Bundle bundle = new Bundle(); 498 bundle.putString(QUERY_TYPE, READ_CHANGE_LOGS_QUERY); 499 bundle.putParcelable(CHANGE_LOGS_REQUEST, request); 500 return bundle; 501 } 502 503 /** Converts a bundle to a ChangeLogsRequest. */ toChangeLogsRequest(Bundle bundle)504 public static ChangeLogsRequest toChangeLogsRequest(Bundle bundle) { 505 return bundle.getParcelable(CHANGE_LOGS_REQUEST, ChangeLogsRequest.class); 506 } 507 508 /** Converts a ChangeLogsResponse to a bundle. */ fromChangeLogsResponse(ChangeLogsResponse response)509 public static Bundle fromChangeLogsResponse(ChangeLogsResponse response) { 510 Bundle bundle = new Bundle(); 511 bundle.putParcelable(CHANGE_LOGS_RESPONSE, response); 512 return bundle; 513 } 514 515 /** Converts a bundle to a ChangeLogsResponse. */ toChangeLogsResponse(Bundle bundle)516 public static ChangeLogsResponse toChangeLogsResponse(Bundle bundle) { 517 return bundle.getParcelable(CHANGE_LOGS_RESPONSE, ChangeLogsResponse.class); 518 } 519 520 /** Converts a {@link CreateMedicalDataSourceRequest} from a bundle. */ toCreateMedicalDataSourceRequest(Bundle bundle)521 public static CreateMedicalDataSourceRequest toCreateMedicalDataSourceRequest(Bundle bundle) { 522 return bundle.getParcelable( 523 CREATE_MEDICAL_DATA_SOURCE_REQUEST, CreateMedicalDataSourceRequest.class); 524 } 525 526 /** Converts a {@link CreateMedicalDataSourceRequest} into a bundle. */ fromCreateMedicalDataSourceRequest( CreateMedicalDataSourceRequest request)527 public static Bundle fromCreateMedicalDataSourceRequest( 528 CreateMedicalDataSourceRequest request) { 529 Bundle bundle = new Bundle(); 530 bundle.putString(QUERY_TYPE, CREATE_MEDICAL_DATA_SOURCE_QUERY); 531 bundle.putParcelable(CREATE_MEDICAL_DATA_SOURCE_REQUEST, request); 532 return bundle; 533 } 534 535 /** Converts one UUID string into a bundle. */ fromMedicalDataSourceId(String id)536 public static Bundle fromMedicalDataSourceId(String id) { 537 Bundle bundle = new Bundle(); 538 bundle.putString(QUERY_TYPE, DELETE_MEDICAL_DATA_SOURCE_WITH_DATA_QUERY); 539 bundle.putString(MEDICAL_DATA_SOURCE_ID, id); 540 return bundle; 541 } 542 543 /** Converts one UUID strings back from a bundle. */ toMedicalDataSourceId(Bundle bundle)544 public static String toMedicalDataSourceId(Bundle bundle) { 545 return bundle.getString(MEDICAL_DATA_SOURCE_ID); 546 } 547 548 /** Converts a list of UUID strings into a bundle. */ fromMedicalDataSourceIds(List<String> ids)549 public static Bundle fromMedicalDataSourceIds(List<String> ids) { 550 Bundle bundle = new Bundle(); 551 bundle.putString(QUERY_TYPE, GET_MEDICAL_DATA_SOURCES_USING_IDS_QUERY); 552 bundle.putStringArrayList(MEDICAL_DATA_SOURCE_ID, new ArrayList<>(ids)); 553 return bundle; 554 } 555 556 /** Converts a list of UUID strings back from a bundle. */ toMedicalDataSourceIds(Bundle bundle)557 public static List<String> toMedicalDataSourceIds(Bundle bundle) { 558 return bundle.getStringArrayList(MEDICAL_DATA_SOURCE_ID); 559 } 560 561 /** Converts a {@link GetMedicalDataSourcesRequest} into a bundle. */ fromMedicalDataSourceRequest(GetMedicalDataSourcesRequest request)562 public static Bundle fromMedicalDataSourceRequest(GetMedicalDataSourcesRequest request) { 563 Bundle bundle = new Bundle(); 564 bundle.putString(QUERY_TYPE, GET_MEDICAL_DATA_SOURCES_USING_REQUEST_QUERY); 565 bundle.putParcelable(GET_MEDICAL_DATA_SOURCES_REQUEST, request); 566 return bundle; 567 } 568 569 /** Converts a {@link GetMedicalDataSourcesRequest} into a bundle. */ toMedicalDataSourceRequest(Bundle bundle)570 public static GetMedicalDataSourcesRequest toMedicalDataSourceRequest(Bundle bundle) { 571 return bundle.getParcelable( 572 GET_MEDICAL_DATA_SOURCES_REQUEST, GetMedicalDataSourcesRequest.class); 573 } 574 575 /** Converts a list of {@link MedicalDataSource}s into a bundle. */ fromMedicalDataSources(List<MedicalDataSource> medicalDataSources)576 public static Bundle fromMedicalDataSources(List<MedicalDataSource> medicalDataSources) { 577 Bundle bundle = new Bundle(); 578 bundle.putParcelableArrayList( 579 MEDICAL_DATA_SOURCES_RESPONSE, new ArrayList<>(medicalDataSources)); 580 return bundle; 581 } 582 583 /** Converts a list of {@link MedicalDataSource}s back from a bundle. */ toMedicalDataSources(Bundle bundle)584 public static List<MedicalDataSource> toMedicalDataSources(Bundle bundle) { 585 return bundle.getParcelableArrayList( 586 MEDICAL_DATA_SOURCES_RESPONSE, MedicalDataSource.class); 587 } 588 589 /** 590 * Converts a {@link MedicalDataSource} to a bundle for sending to another app. 591 * 592 * <p>To convert back, use {@link #toMedicalDataSource(Bundle)}. 593 */ fromMedicalDataSource(MedicalDataSource medicalDataSource)594 public static Bundle fromMedicalDataSource(MedicalDataSource medicalDataSource) { 595 Bundle bundle = new Bundle(); 596 bundle.putParcelable(MEDICAL_DATA_SOURCE_RESPONSE, medicalDataSource); 597 return bundle; 598 } 599 600 /** 601 * Converts a {@link MedicalDataSource} back from a bundle. 602 * 603 * <p>To create, use {@link #fromMedicalDataSource(MedicalDataSource)}. 604 */ toMedicalDataSource(Bundle bundle)605 public static MedicalDataSource toMedicalDataSource(Bundle bundle) { 606 return bundle.getParcelable(MEDICAL_DATA_SOURCE_RESPONSE, MedicalDataSource.class); 607 } 608 609 /** Converts a {@link CreateMedicalDataSourceRequest} from a bundle. */ toUpsertMedicalResourceRequests( Bundle bundle)610 public static List<UpsertMedicalResourceRequest> toUpsertMedicalResourceRequests( 611 Bundle bundle) { 612 return bundle.getParcelableArrayList( 613 UPSERT_MEDICAL_RESOURCE_REQUESTS, UpsertMedicalResourceRequest.class); 614 } 615 616 /** Converts a {@link CreateMedicalDataSourceRequest} into a bundle. */ fromUpsertMedicalResourceRequests( List<UpsertMedicalResourceRequest> requests)617 public static Bundle fromUpsertMedicalResourceRequests( 618 List<UpsertMedicalResourceRequest> requests) { 619 Bundle bundle = new Bundle(); 620 bundle.putString(QUERY_TYPE, UPSERT_MEDICAL_RESOURCES_QUERY); 621 bundle.putParcelableArrayList(UPSERT_MEDICAL_RESOURCE_REQUESTS, new ArrayList<>(requests)); 622 return bundle; 623 } 624 625 /** Converts a {@link ReadMedicalResourcesRequest} into a bundle. */ fromReadMedicalResourcesRequest(ReadMedicalResourcesRequest request)626 public static Bundle fromReadMedicalResourcesRequest(ReadMedicalResourcesRequest request) { 627 Bundle bundle = new Bundle(); 628 bundle.putString(QUERY_TYPE, READ_MEDICAL_RESOURCES_BY_REQUEST_QUERY); 629 bundle.putInt(READ_MEDICAL_RESOURCES_REQUEST_PAGE_SIZE, request.getPageSize()); 630 631 if (request instanceof ReadMedicalResourcesPageRequest) { 632 bundle.putBoolean(READ_MEDICAL_RESOURCES_REQUEST_IS_PAGE_REQUEST, true); 633 bundle.putString( 634 READ_MEDICAL_RESOURCES_REQUEST_PAGE_TOKEN, 635 ((ReadMedicalResourcesPageRequest) request).getPageToken()); 636 } else if (request instanceof ReadMedicalResourcesInitialRequest) { 637 ReadMedicalResourcesInitialRequest initialRequest = 638 (ReadMedicalResourcesInitialRequest) request; 639 bundle.putBoolean(READ_MEDICAL_RESOURCES_REQUEST_IS_PAGE_REQUEST, false); 640 bundle.putInt( 641 READ_MEDICAL_RESOURCES_REQUEST_MEDICAL_RESOURCE_TYPE, 642 initialRequest.getMedicalResourceType()); 643 bundle.putStringArrayList( 644 READ_MEDICAL_RESOURCES_REQUEST_DATA_SOURCE_IDS, 645 new ArrayList<>(initialRequest.getDataSourceIds())); 646 } else { 647 throw new IllegalArgumentException( 648 "Request was not of type ReadMedicalResourcesInitialRequest or" 649 + " ReadMedicalResourcesPageRequest"); 650 } 651 652 // Check that no data was lost and that the request can be restored again. This could happen 653 // if new fields are added to the ReadMedicalResourcesRequest without including them here. 654 if (!toReadMedicalResourcesRequest(bundle).equals(request)) { 655 throw new IllegalStateException("Data may be lost when converting to/from Bundle"); 656 } 657 658 return bundle; 659 } 660 661 /** Converts a {@link ReadMedicalResourcesRequest} from a bundle. */ toReadMedicalResourcesRequest(Bundle bundle)662 public static ReadMedicalResourcesRequest toReadMedicalResourcesRequest(Bundle bundle) { 663 boolean isPageRequest = bundle.getBoolean(READ_MEDICAL_RESOURCES_REQUEST_IS_PAGE_REQUEST); 664 int pageSize = bundle.getInt(READ_MEDICAL_RESOURCES_REQUEST_PAGE_SIZE); 665 666 if (isPageRequest) { 667 String pageToken = bundle.getString(READ_MEDICAL_RESOURCES_REQUEST_PAGE_TOKEN); 668 return new ReadMedicalResourcesPageRequest.Builder(pageToken) 669 .setPageSize(pageSize) 670 .build(); 671 } else { 672 int medicalResourceType = 673 bundle.getInt(READ_MEDICAL_RESOURCES_REQUEST_MEDICAL_RESOURCE_TYPE); 674 Set<String> dataSourceIds = 675 new HashSet<>( 676 bundle.getStringArrayList( 677 READ_MEDICAL_RESOURCES_REQUEST_DATA_SOURCE_IDS)); 678 return new ReadMedicalResourcesInitialRequest.Builder(medicalResourceType) 679 .addDataSourceIds(dataSourceIds) 680 .setPageSize(pageSize) 681 .build(); 682 } 683 } 684 685 /** Converts a list of {@link MedicalResourceId}s from a bundle. */ toMedicalResourceIds(Bundle bundle)686 public static List<MedicalResourceId> toMedicalResourceIds(Bundle bundle) { 687 return bundle.getParcelableArrayList(MEDICAL_RESOURCE_IDS, MedicalResourceId.class); 688 } 689 690 /** 691 * Converts a list of {@link MedicalResourceId}s into a bundle with QUERY_TYPE set to 692 * READ_MEDICAL_RESOURCES_BY_IDS_QUERY 693 */ fromMedicalResourceIdsForRead(List<MedicalResourceId> ids)694 public static Bundle fromMedicalResourceIdsForRead(List<MedicalResourceId> ids) { 695 Bundle bundle = new Bundle(); 696 bundle.putString(QUERY_TYPE, READ_MEDICAL_RESOURCES_BY_IDS_QUERY); 697 bundle.putParcelableArrayList(MEDICAL_RESOURCE_IDS, new ArrayList<>(ids)); 698 return bundle; 699 } 700 701 /** 702 * Converts a list of {@link MedicalResourceId}s into a bundle with QUERY_TYPE set to 703 * DELETE_MEDICAL_RESOURCES_BY_IDS_QUERY 704 */ fromMedicalResourceIdsForDelete(List<MedicalResourceId> ids)705 public static Bundle fromMedicalResourceIdsForDelete(List<MedicalResourceId> ids) { 706 Bundle bundle = new Bundle(); 707 bundle.putString(QUERY_TYPE, DELETE_MEDICAL_RESOURCES_BY_IDS_QUERY); 708 bundle.putParcelableArrayList(MEDICAL_RESOURCE_IDS, new ArrayList<>(ids)); 709 return bundle; 710 } 711 712 /** 713 * Converts a list of {@link MedicalResource}s to a bundle for sending to another app. 714 * 715 * <p>To convert back, use {@link #toMedicalResources(Bundle)}. 716 */ fromMedicalResources(List<MedicalResource> medicalResources)717 public static Bundle fromMedicalResources(List<MedicalResource> medicalResources) { 718 Bundle bundle = new Bundle(); 719 bundle.putParcelableArrayList( 720 MEDICAL_RESOURCES_RESPONSE, new ArrayList<>(medicalResources)); 721 return bundle; 722 } 723 724 /** 725 * Converts a list of {@link MedicalResource}s back from a bundle. 726 * 727 * <p>To create, use {@link #fromMedicalResources(List)}. 728 */ toMedicalResources(Bundle bundle)729 public static List<MedicalResource> toMedicalResources(Bundle bundle) { 730 return bundle.getParcelableArrayList(MEDICAL_RESOURCES_RESPONSE, MedicalResource.class); 731 } 732 733 /** Converts a {@link ReadMedicalResourcesResponse} from a bundle. */ toReadMedicalResourcesResponse(Bundle bundle)734 public static ReadMedicalResourcesResponse toReadMedicalResourcesResponse(Bundle bundle) { 735 return bundle.getParcelable( 736 READ_MEDICAL_RESOURCES_RESPONSE, ReadMedicalResourcesResponse.class); 737 } 738 739 /** Converts a {@link ReadMedicalResourcesResponse} to a bundle for sending to another app. */ fromReadMedicalResourcesResponse(ReadMedicalResourcesResponse response)740 public static Bundle fromReadMedicalResourcesResponse(ReadMedicalResourcesResponse response) { 741 Bundle bundle = new Bundle(); 742 bundle.putParcelable(READ_MEDICAL_RESOURCES_RESPONSE, response); 743 return bundle; 744 } 745 746 /** Converts a {@link DeleteMedicalResourcesRequest} from a bundle. */ toDeleteMedicalResourcesRequest(Bundle bundle)747 public static DeleteMedicalResourcesRequest toDeleteMedicalResourcesRequest(Bundle bundle) { 748 return bundle.getParcelable( 749 DELETE_MEDICAL_RESOURCES_REQUEST, DeleteMedicalResourcesRequest.class); 750 } 751 752 /** Converts a {@link DeleteMedicalResourcesRequest} into a bundle. */ fromDeleteMedicalResourcesRequest(DeleteMedicalResourcesRequest request)753 public static Bundle fromDeleteMedicalResourcesRequest(DeleteMedicalResourcesRequest request) { 754 Bundle bundle = new Bundle(); 755 bundle.putString(QUERY_TYPE, DELETE_MEDICAL_RESOURCES_BY_REQUEST_QUERY); 756 bundle.putParcelable(DELETE_MEDICAL_RESOURCES_REQUEST, request); 757 return bundle; 758 } 759 fromRecordList(List<? extends Record> records)760 private static List<Bundle> fromRecordList(List<? extends Record> records) { 761 return records.stream().map(BundleHelper::fromRecord).toList(); 762 } 763 toRecordList(List<Bundle> bundles)764 private static List<? extends Record> toRecordList(List<Bundle> bundles) { 765 return bundles.stream().map(BundleHelper::toRecord).toList(); 766 } 767 fromRecord(Record record)768 private static Bundle fromRecord(Record record) { 769 Bundle bundle = new Bundle(); 770 bundle.putString(RECORD_CLASS_NAME, record.getClass().getName()); 771 bundle.putBundle(METADATA, fromMetadata(record.getMetadata())); 772 773 if (record instanceof IntervalRecord intervalRecord) { 774 bundle.putLong(START_TIME_MILLIS, intervalRecord.getStartTime().toEpochMilli()); 775 bundle.putLong(END_TIME_MILLIS, intervalRecord.getEndTime().toEpochMilli()); 776 bundle.putInt(START_ZONE_OFFSET, intervalRecord.getStartZoneOffset().getTotalSeconds()); 777 bundle.putInt(END_ZONE_OFFSET, intervalRecord.getEndZoneOffset().getTotalSeconds()); 778 } else if (record instanceof InstantRecord instantRecord) { 779 bundle.putLong(START_TIME_MILLIS, instantRecord.getTime().toEpochMilli()); 780 bundle.putInt(START_ZONE_OFFSET, instantRecord.getZoneOffset().getTotalSeconds()); 781 } else { 782 throw new IllegalArgumentException("Unsupported record type: "); 783 } 784 785 Bundle values; 786 787 RecordFactory<? extends Record> recordFactory = 788 RecordFactory.forDataType(record.getClass()); 789 790 if (recordFactory != null) { 791 values = recordFactory.getValuesBundle(record); 792 } else if (record instanceof BasalMetabolicRateRecord basalMetabolicRateRecord) { 793 values = getBasalMetabolicRateRecordValues(basalMetabolicRateRecord); 794 } else if (record instanceof ExerciseSessionRecord exerciseSessionRecord) { 795 values = getExerciseSessionRecordValues(exerciseSessionRecord); 796 } else if (record instanceof StepsRecord stepsRecord) { 797 values = getStepsRecordValues(stepsRecord); 798 } else if (record instanceof HeartRateRecord heartRateRecord) { 799 values = getHeartRateRecordValues(heartRateRecord); 800 } else if (record instanceof SleepSessionRecord sleepSessionRecord) { 801 values = getSleepRecordValues(sleepSessionRecord); 802 } else if (record instanceof DistanceRecord distanceRecord) { 803 values = getDistanceRecordValues(distanceRecord); 804 } else if (record instanceof TotalCaloriesBurnedRecord totalCaloriesBurnedRecord) { 805 values = getTotalCaloriesBurnedRecord(totalCaloriesBurnedRecord); 806 } else if (record instanceof MenstruationPeriodRecord) { 807 values = new Bundle(); 808 } else if (record instanceof WeightRecord weightRecord) { 809 values = getWeightRecord(weightRecord); 810 } else if (record instanceof PlannedExerciseSessionRecord plannedExerciseSessionRecord) { 811 values = getPlannedExerciseSessionRecord(plannedExerciseSessionRecord); 812 } else if (record instanceof NutritionRecord nutritionRecord) { 813 values = getNutritionRecord(nutritionRecord); 814 } else { 815 throw new IllegalArgumentException( 816 "Unsupported record type: " + record.getClass().getName()); 817 } 818 819 bundle.putBundle(VALUES, values); 820 821 Record decodedRecord = toRecord(bundle); 822 if (!record.equals(decodedRecord)) { 823 Log.e( 824 TAG, 825 BundleHelper.class.getSimpleName() 826 + ".java - record = " 827 + ToStringUtils.recordToString(record)); 828 Log.e( 829 TAG, 830 BundleHelper.class.getSimpleName() 831 + ".java - decoded = " 832 + ToStringUtils.recordToString(record)); 833 throw new IllegalArgumentException( 834 "Some fields are incorrectly encoded in " + record.getClass().getSimpleName()); 835 } 836 837 return bundle; 838 } 839 toRecord(Bundle bundle)840 private static Record toRecord(Bundle bundle) { 841 Metadata metadata = toMetadata(bundle.getBundle(METADATA)); 842 843 String recordClassName = bundle.getString(RECORD_CLASS_NAME); 844 845 Instant startTime = Instant.ofEpochMilli(bundle.getLong(START_TIME_MILLIS)); 846 Instant endTime = Instant.ofEpochMilli(bundle.getLong(END_TIME_MILLIS)); 847 ZoneOffset startZoneOffset = ZoneOffset.ofTotalSeconds(bundle.getInt(START_ZONE_OFFSET)); 848 ZoneOffset endZoneOffset = ZoneOffset.ofTotalSeconds(bundle.getInt(END_ZONE_OFFSET)); 849 850 Bundle values = bundle.getBundle(VALUES); 851 852 Class<? extends Record> recordClass = recordClassForName(recordClassName); 853 RecordFactory<? extends Record> recordFactory = RecordFactory.forDataType(recordClass); 854 855 if (recordFactory != null) { 856 return recordFactory.newRecordFromValuesBundle( 857 metadata, startTime, endTime, startZoneOffset, endZoneOffset, values); 858 } else if (Objects.equals(recordClassName, BasalMetabolicRateRecord.class.getName())) { 859 return createBasalMetabolicRateRecord(metadata, startTime, startZoneOffset, values); 860 } else if (Objects.equals(recordClassName, ExerciseSessionRecord.class.getName())) { 861 return createExerciseSessionRecord( 862 metadata, startTime, endTime, startZoneOffset, endZoneOffset, values); 863 } else if (Objects.equals(recordClassName, HeartRateRecord.class.getName())) { 864 return createHeartRateRecord( 865 metadata, startTime, endTime, startZoneOffset, endZoneOffset, values); 866 } else if (Objects.equals(recordClassName, StepsRecord.class.getName())) { 867 return createStepsRecord( 868 metadata, startTime, endTime, startZoneOffset, endZoneOffset, values); 869 } else if (Objects.equals(recordClassName, SleepSessionRecord.class.getName())) { 870 return createSleepSessionRecord( 871 metadata, startTime, endTime, startZoneOffset, endZoneOffset, values); 872 } else if (Objects.equals(recordClassName, DistanceRecord.class.getName())) { 873 return createDistanceRecord( 874 metadata, startTime, endTime, startZoneOffset, endZoneOffset, values); 875 } else if (Objects.equals(recordClassName, TotalCaloriesBurnedRecord.class.getName())) { 876 return createTotalCaloriesBurnedRecord( 877 metadata, startTime, endTime, startZoneOffset, endZoneOffset, values); 878 } else if (Objects.equals(recordClassName, MenstruationPeriodRecord.class.getName())) { 879 return new MenstruationPeriodRecord.Builder(metadata, startTime, endTime).build(); 880 } else if (Objects.equals(recordClassName, WeightRecord.class.getName())) { 881 return createWeightRecord(metadata, startTime, startZoneOffset, values); 882 } else if (Objects.equals(recordClassName, PlannedExerciseSessionRecord.class.getName())) { 883 return createPlannedExerciseSessionRecord( 884 metadata, startTime, endTime, startZoneOffset, endZoneOffset, values); 885 } else if (Objects.equals(recordClassName, NutritionRecord.class.getName())) { 886 return createNutritionRecord( 887 metadata, startTime, endTime, startZoneOffset, endZoneOffset, values); 888 } 889 890 throw new IllegalArgumentException("Unsupported record type: " + recordClassName); 891 } 892 getBasalMetabolicRateRecordValues(BasalMetabolicRateRecord record)893 private static Bundle getBasalMetabolicRateRecordValues(BasalMetabolicRateRecord record) { 894 Bundle values = new Bundle(); 895 values.putDouble(POWER_WATTS, record.getBasalMetabolicRate().getInWatts()); 896 return values; 897 } 898 createBasalMetabolicRateRecord( Metadata metadata, Instant time, ZoneOffset startZoneOffset, Bundle values)899 private static BasalMetabolicRateRecord createBasalMetabolicRateRecord( 900 Metadata metadata, Instant time, ZoneOffset startZoneOffset, Bundle values) { 901 double powerWatts = values.getDouble(POWER_WATTS); 902 903 return new BasalMetabolicRateRecord.Builder(metadata, time, Power.fromWatts(powerWatts)) 904 .setZoneOffset(startZoneOffset) 905 .build(); 906 } 907 getExerciseSessionRecordValues(ExerciseSessionRecord record)908 private static Bundle getExerciseSessionRecordValues(ExerciseSessionRecord record) { 909 Bundle values = new Bundle(); 910 911 values.putInt(EXERCISE_SESSION_TYPE, record.getExerciseType()); 912 913 ExerciseRoute route = record.getRoute(); 914 915 if (route != null) { 916 long[] timestamps = 917 route.getRouteLocations().stream() 918 .map(ExerciseRoute.Location::getTime) 919 .mapToLong(Instant::toEpochMilli) 920 .toArray(); 921 double[] latitudes = 922 route.getRouteLocations().stream() 923 .mapToDouble(ExerciseRoute.Location::getLatitude) 924 .toArray(); 925 double[] longitudes = 926 route.getRouteLocations().stream() 927 .mapToDouble(ExerciseRoute.Location::getLongitude) 928 .toArray(); 929 List<Double> altitudes = 930 route.getRouteLocations().stream() 931 .map(ExerciseRoute.Location::getAltitude) 932 .map(alt -> transformOrNull(alt, Length::getInMeters)) 933 .toList(); 934 List<Double> hAccs = 935 route.getRouteLocations().stream() 936 .map(ExerciseRoute.Location::getHorizontalAccuracy) 937 .map(hAcc -> transformOrNull(hAcc, Length::getInMeters)) 938 .toList(); 939 List<Double> vAccs = 940 route.getRouteLocations().stream() 941 .map(ExerciseRoute.Location::getVerticalAccuracy) 942 .map(vAcc -> transformOrNull(vAcc, Length::getInMeters)) 943 .toList(); 944 945 values.putLongArray(EXERCISE_ROUTE_TIMESTAMPS, timestamps); 946 values.putDoubleArray(EXERCISE_ROUTE_LATITUDES, latitudes); 947 values.putDoubleArray(EXERCISE_ROUTE_LONGITUDES, longitudes); 948 values.putSerializable(EXERCISE_ROUTE_ALTITUDES, new ArrayList<>(altitudes)); 949 values.putSerializable(EXERCISE_ROUTE_HACCS, new ArrayList<>(hAccs)); 950 values.putSerializable(EXERCISE_ROUTE_VACCS, new ArrayList<>(vAccs)); 951 } 952 953 values.putBoolean(EXERCISE_HAS_ROUTE, record.hasRoute()); 954 955 long[] segmentStartTimes = 956 record.getSegments().stream() 957 .map(ExerciseSegment::getStartTime) 958 .mapToLong(Instant::toEpochMilli) 959 .toArray(); 960 long[] segmentEndTimes = 961 record.getSegments().stream() 962 .map(ExerciseSegment::getEndTime) 963 .mapToLong(Instant::toEpochMilli) 964 .toArray(); 965 int[] segmentTypes = 966 record.getSegments().stream().mapToInt(ExerciseSegment::getSegmentType).toArray(); 967 int[] repCounts = 968 record.getSegments().stream() 969 .mapToInt(ExerciseSegment::getRepetitionsCount) 970 .toArray(); 971 972 values.putLongArray(START_TIMES, segmentStartTimes); 973 values.putLongArray(END_TIMES, segmentEndTimes); 974 values.putIntArray(EXERCISE_SEGMENT_TYPES, segmentTypes); 975 values.putIntArray(EXERCISE_SEGMENT_REP_COUNTS, repCounts); 976 977 List<ExerciseLap> laps = record.getLaps(); 978 if (laps != null && !laps.isEmpty()) { 979 Bundle lapsBundle = new Bundle(); 980 lapsBundle.putLongArray( 981 START_TIMES, 982 laps.stream() 983 .map(ExerciseLap::getStartTime) 984 .mapToLong(Instant::toEpochMilli) 985 .toArray()); 986 lapsBundle.putLongArray( 987 END_TIMES, 988 laps.stream() 989 .map(ExerciseLap::getEndTime) 990 .mapToLong(Instant::toEpochMilli) 991 .toArray()); 992 lapsBundle.putDoubleArray( 993 LENGTH_IN_METERS, 994 laps.stream() 995 .map(ExerciseLap::getLength) 996 .map(length -> length == null ? -1 : length.getInMeters()) 997 .mapToDouble(value -> value) 998 .toArray()); 999 values.putBundle(EXERCISE_LAPS, lapsBundle); 1000 } 1001 1002 values.putCharSequence(TITLE, record.getTitle()); 1003 values.putCharSequence(NOTES, record.getNotes()); 1004 values.putString(PLANNED_EXERCISE_SESSION_ID, record.getPlannedExerciseSessionId()); 1005 1006 return values; 1007 } 1008 createExerciseSessionRecord( Metadata metadata, Instant startTime, Instant endTime, ZoneOffset startZoneOffset, ZoneOffset endZoneOffset, Bundle values)1009 private static ExerciseSessionRecord createExerciseSessionRecord( 1010 Metadata metadata, 1011 Instant startTime, 1012 Instant endTime, 1013 ZoneOffset startZoneOffset, 1014 ZoneOffset endZoneOffset, 1015 Bundle values) { 1016 int exerciseType = values.getInt(EXERCISE_SESSION_TYPE); 1017 1018 ExerciseSessionRecord.Builder record = 1019 new ExerciseSessionRecord.Builder(metadata, startTime, endTime, exerciseType); 1020 1021 long[] routeTimestamps = values.getLongArray(EXERCISE_ROUTE_TIMESTAMPS); 1022 1023 int locationCount = routeTimestamps == null ? 0 : routeTimestamps.length; 1024 1025 if (locationCount > 0) { 1026 double[] latitudes = values.getDoubleArray(EXERCISE_ROUTE_LATITUDES); 1027 double[] longitudes = values.getDoubleArray(EXERCISE_ROUTE_LONGITUDES); 1028 List<Double> altitudes = 1029 values.getSerializable(EXERCISE_ROUTE_ALTITUDES, ArrayList.class); 1030 List<Double> hAccs = values.getSerializable(EXERCISE_ROUTE_HACCS, ArrayList.class); 1031 List<Double> vAccs = values.getSerializable(EXERCISE_ROUTE_VACCS, ArrayList.class); 1032 List<ExerciseRoute.Location> locations = 1033 IntStream.range(0, locationCount) 1034 .mapToObj( 1035 i -> { 1036 Instant time = Instant.ofEpochMilli(routeTimestamps[i]); 1037 double latitude = latitudes[i]; 1038 double longitude = longitudes[i]; 1039 Double altitude = altitudes.get(i); 1040 Double hAcc = hAccs.get(i); 1041 Double vAcc = vAccs.get(i); 1042 1043 var location = 1044 new ExerciseRoute.Location.Builder( 1045 time, latitude, longitude); 1046 1047 if (altitude != null) { 1048 location.setAltitude(Length.fromMeters(altitude)); 1049 } 1050 1051 if (hAcc != null) { 1052 location.setHorizontalAccuracy(Length.fromMeters(hAcc)); 1053 } 1054 1055 if (vAcc != null) { 1056 location.setVerticalAccuracy(Length.fromMeters(vAcc)); 1057 } 1058 1059 return location.build(); 1060 }) 1061 .toList(); 1062 1063 record.setRoute(new ExerciseRoute(locations)); 1064 } 1065 1066 boolean hasRoute = values.getBoolean(EXERCISE_HAS_ROUTE); 1067 1068 if (hasRoute && locationCount == 0) { 1069 // Handle the `route == null && hasRoute == true` case which is a valid state. 1070 setHasRoute(record, hasRoute); 1071 } 1072 1073 long[] segmentStartTimes = values.getLongArray(START_TIMES); 1074 long[] segmentEndTimes = values.getLongArray(END_TIMES); 1075 int[] segmentTypes = values.getIntArray(EXERCISE_SEGMENT_TYPES); 1076 int[] repCounts = values.getIntArray(EXERCISE_SEGMENT_REP_COUNTS); 1077 1078 List<ExerciseSegment> segments = 1079 IntStream.range(0, segmentStartTimes.length) 1080 .mapToObj( 1081 i -> { 1082 Instant segmentStartTime = 1083 Instant.ofEpochMilli(segmentStartTimes[i]); 1084 Instant segmentEndTime = 1085 Instant.ofEpochMilli(segmentEndTimes[i]); 1086 return new ExerciseSegment.Builder( 1087 segmentStartTime, 1088 segmentEndTime, 1089 segmentTypes[i]) 1090 .setRepetitionsCount(repCounts[i]) 1091 .build(); 1092 }) 1093 .toList(); 1094 1095 record.setSegments(segments); 1096 1097 Bundle lapsBundle = values.getBundle(EXERCISE_LAPS); 1098 if (lapsBundle != null) { 1099 List<ExerciseLap> laps = new ArrayList<>(); 1100 double[] lengths = lapsBundle.getDoubleArray(LENGTH_IN_METERS); 1101 long[] startTimes = lapsBundle.getLongArray(START_TIMES); 1102 long[] endTimes = lapsBundle.getLongArray(END_TIMES); 1103 for (int i = 0; i < lengths.length; i++) { 1104 ExerciseLap.Builder lap = 1105 new ExerciseLap.Builder( 1106 Instant.ofEpochMilli(startTimes[i]), 1107 Instant.ofEpochMilli(endTimes[i])); 1108 if (lengths[i] > 0) { 1109 lap.setLength(Length.fromMeters(lengths[i])); 1110 } 1111 laps.add(lap.build()); 1112 } 1113 record.setLaps(laps); 1114 } 1115 1116 record.setTitle(values.getCharSequence(TITLE)); 1117 record.setNotes(values.getCharSequence(NOTES)); 1118 record.setPlannedExerciseSessionId(values.getString(PLANNED_EXERCISE_SESSION_ID)); 1119 record.setStartZoneOffset(startZoneOffset); 1120 record.setEndZoneOffset(endZoneOffset); 1121 1122 return record.build(); 1123 } 1124 getHeartRateRecordValues(HeartRateRecord record)1125 private static Bundle getHeartRateRecordValues(HeartRateRecord record) { 1126 Bundle values = new Bundle(); 1127 long[] times = 1128 record.getSamples().stream() 1129 .map(HeartRateRecord.HeartRateSample::getTime) 1130 .mapToLong(Instant::toEpochMilli) 1131 .toArray(); 1132 long[] bpms = 1133 record.getSamples().stream() 1134 .mapToLong(HeartRateRecord.HeartRateSample::getBeatsPerMinute) 1135 .toArray(); 1136 1137 values.putLongArray(SAMPLE_TIMES, times); 1138 values.putLongArray(SAMPLE_VALUES, bpms); 1139 return values; 1140 } 1141 getSleepRecordValues(SleepSessionRecord record)1142 private static Bundle getSleepRecordValues(SleepSessionRecord record) { 1143 Bundle values = new Bundle(); 1144 values.putLongArray( 1145 START_TIMES, 1146 record.getStages().stream() 1147 .map(Stage::getStartTime) 1148 .mapToLong(Instant::toEpochMilli) 1149 .toArray()); 1150 values.putLongArray( 1151 END_TIMES, 1152 record.getStages().stream() 1153 .map(Stage::getEndTime) 1154 .mapToLong(Instant::toEpochMilli) 1155 .toArray()); 1156 values.putIntArray( 1157 SAMPLE_VALUES, record.getStages().stream().mapToInt(Stage::getType).toArray()); 1158 values.putCharSequence(NOTES, record.getNotes()); 1159 values.putCharSequence(TITLE, record.getTitle()); 1160 return values; 1161 } 1162 getDistanceRecordValues(DistanceRecord record)1163 private static Bundle getDistanceRecordValues(DistanceRecord record) { 1164 Bundle values = new Bundle(); 1165 values.putDouble(LENGTH_IN_METERS, record.getDistance().getInMeters()); 1166 return values; 1167 } 1168 getTotalCaloriesBurnedRecord(TotalCaloriesBurnedRecord record)1169 private static Bundle getTotalCaloriesBurnedRecord(TotalCaloriesBurnedRecord record) { 1170 Bundle values = new Bundle(); 1171 values.putDouble(ENERGY_IN_CALORIES, record.getEnergy().getInCalories()); 1172 return values; 1173 } 1174 getWeightRecord(WeightRecord record)1175 private static Bundle getWeightRecord(WeightRecord record) { 1176 Bundle values = new Bundle(); 1177 values.putDouble(WEIGHT_IN_GRAMS, record.getWeight().getInGrams()); 1178 return values; 1179 } 1180 getPlannedExerciseSessionRecord(PlannedExerciseSessionRecord record)1181 private static Bundle getPlannedExerciseSessionRecord(PlannedExerciseSessionRecord record) { 1182 Bundle values = new Bundle(); 1183 values.putInt(EXERCISE_SESSION_TYPE, record.getExerciseType()); 1184 return values; 1185 } 1186 getNutritionRecord(NutritionRecord record)1187 private static Bundle getNutritionRecord(NutritionRecord record) { 1188 Bundle values = new Bundle(); 1189 if (record.getIron() != null) { 1190 values.putDouble(IRON_IN_GRAMS, record.getIron().getInGrams()); 1191 } 1192 return values; 1193 } 1194 createHeartRateRecord( Metadata metadata, Instant startTime, Instant endTime, ZoneOffset startZoneOffset, ZoneOffset endZoneOffset, Bundle values)1195 private static HeartRateRecord createHeartRateRecord( 1196 Metadata metadata, 1197 Instant startTime, 1198 Instant endTime, 1199 ZoneOffset startZoneOffset, 1200 ZoneOffset endZoneOffset, 1201 Bundle values) { 1202 1203 long[] times = values.getLongArray(SAMPLE_TIMES); 1204 long[] bpms = values.getLongArray(SAMPLE_VALUES); 1205 1206 List<HeartRateRecord.HeartRateSample> samples = 1207 IntStream.range(0, times.length) 1208 .mapToObj( 1209 i -> 1210 new HeartRateRecord.HeartRateSample( 1211 bpms[i], Instant.ofEpochMilli(times[i]))) 1212 .toList(); 1213 1214 return new HeartRateRecord.Builder(metadata, startTime, endTime, samples) 1215 .setStartZoneOffset(startZoneOffset) 1216 .setEndZoneOffset(endZoneOffset) 1217 .build(); 1218 } 1219 getStepsRecordValues(StepsRecord record)1220 private static Bundle getStepsRecordValues(StepsRecord record) { 1221 Bundle values = new Bundle(); 1222 values.putLong(COUNT, record.getCount()); 1223 return values; 1224 } 1225 createStepsRecord( Metadata metadata, Instant startTime, Instant endTime, ZoneOffset startZoneOffset, ZoneOffset endZoneOffset, Bundle values)1226 private static StepsRecord createStepsRecord( 1227 Metadata metadata, 1228 Instant startTime, 1229 Instant endTime, 1230 ZoneOffset startZoneOffset, 1231 ZoneOffset endZoneOffset, 1232 Bundle values) { 1233 long count = values.getLong(COUNT); 1234 1235 return new StepsRecord.Builder(metadata, startTime, endTime, count) 1236 .setStartZoneOffset(startZoneOffset) 1237 .setEndZoneOffset(endZoneOffset) 1238 .build(); 1239 } 1240 createSleepSessionRecord( Metadata metadata, Instant startTime, Instant endTime, ZoneOffset startZoneOffset, ZoneOffset endZoneOffset, Bundle values)1241 private static SleepSessionRecord createSleepSessionRecord( 1242 Metadata metadata, 1243 Instant startTime, 1244 Instant endTime, 1245 ZoneOffset startZoneOffset, 1246 ZoneOffset endZoneOffset, 1247 Bundle values) { 1248 List<Stage> stages = new ArrayList<>(); 1249 int[] stageInts = values.getIntArray(SAMPLE_VALUES); 1250 long[] startTimeMillis = values.getLongArray(START_TIMES); 1251 long[] endTimeMillis = values.getLongArray(END_TIMES); 1252 for (int i = 0; i < stageInts.length; i++) { 1253 stages.add( 1254 new Stage( 1255 Instant.ofEpochMilli(startTimeMillis[i]), 1256 Instant.ofEpochMilli(endTimeMillis[i]), 1257 stageInts[i])); 1258 } 1259 return new SleepSessionRecord.Builder(metadata, startTime, endTime) 1260 .setStages(stages) 1261 .setNotes(values.getCharSequence(NOTES)) 1262 .setTitle(values.getCharSequence(TITLE)) 1263 .setStartZoneOffset(startZoneOffset) 1264 .setEndZoneOffset(endZoneOffset) 1265 .build(); 1266 } 1267 createDistanceRecord( Metadata metadata, Instant startTime, Instant endTime, ZoneOffset startZoneOffset, ZoneOffset endZoneOffset, Bundle values)1268 private static DistanceRecord createDistanceRecord( 1269 Metadata metadata, 1270 Instant startTime, 1271 Instant endTime, 1272 ZoneOffset startZoneOffset, 1273 ZoneOffset endZoneOffset, 1274 Bundle values) { 1275 double lengthInMeters = values.getDouble(LENGTH_IN_METERS); 1276 return new DistanceRecord.Builder( 1277 metadata, startTime, endTime, Length.fromMeters(lengthInMeters)) 1278 .setStartZoneOffset(startZoneOffset) 1279 .setEndZoneOffset(endZoneOffset) 1280 .build(); 1281 } 1282 createTotalCaloriesBurnedRecord( Metadata metadata, Instant startTime, Instant endTime, ZoneOffset startZoneOffset, ZoneOffset endZoneOffset, Bundle values)1283 private static TotalCaloriesBurnedRecord createTotalCaloriesBurnedRecord( 1284 Metadata metadata, 1285 Instant startTime, 1286 Instant endTime, 1287 ZoneOffset startZoneOffset, 1288 ZoneOffset endZoneOffset, 1289 Bundle values) { 1290 double energyInCalories = values.getDouble(ENERGY_IN_CALORIES); 1291 return new TotalCaloriesBurnedRecord.Builder( 1292 metadata, startTime, endTime, Energy.fromCalories(energyInCalories)) 1293 .setStartZoneOffset(startZoneOffset) 1294 .setEndZoneOffset(endZoneOffset) 1295 .build(); 1296 } 1297 createWeightRecord( Metadata metadata, Instant time, ZoneOffset zoneOffset, Bundle values)1298 private static WeightRecord createWeightRecord( 1299 Metadata metadata, Instant time, ZoneOffset zoneOffset, Bundle values) { 1300 double weightInGrams = values.getDouble(WEIGHT_IN_GRAMS); 1301 return new WeightRecord.Builder(metadata, time, Mass.fromGrams(weightInGrams)) 1302 .setZoneOffset(zoneOffset) 1303 .build(); 1304 } 1305 createPlannedExerciseSessionRecord( Metadata metadata, Instant startTime, Instant endTime, ZoneOffset startZoneOffset, ZoneOffset endZoneOffset, Bundle values)1306 private static PlannedExerciseSessionRecord createPlannedExerciseSessionRecord( 1307 Metadata metadata, 1308 Instant startTime, 1309 Instant endTime, 1310 ZoneOffset startZoneOffset, 1311 ZoneOffset endZoneOffset, 1312 Bundle values) { 1313 int exerciseType = values.getInt(EXERCISE_SESSION_TYPE); 1314 return new PlannedExerciseSessionRecord.Builder(metadata, exerciseType, startTime, endTime) 1315 .setStartZoneOffset(startZoneOffset) 1316 .setEndZoneOffset(endZoneOffset) 1317 .build(); 1318 } 1319 createNutritionRecord( Metadata metadata, Instant startTime, Instant endTime, ZoneOffset startZoneOffset, ZoneOffset endZoneOffset, Bundle values)1320 private static NutritionRecord createNutritionRecord( 1321 Metadata metadata, 1322 Instant startTime, 1323 Instant endTime, 1324 ZoneOffset startZoneOffset, 1325 ZoneOffset endZoneOffset, 1326 Bundle values) { 1327 NutritionRecord.Builder record = 1328 new NutritionRecord.Builder(metadata, startTime, endTime) 1329 .setStartZoneOffset(startZoneOffset) 1330 .setEndZoneOffset(endZoneOffset); 1331 if (values.containsKey(IRON_IN_GRAMS)) { 1332 record.setIron(Mass.fromGrams(values.getDouble(IRON_IN_GRAMS))); 1333 } 1334 return record.build(); 1335 } 1336 fromMetadata(Metadata metadata)1337 private static Bundle fromMetadata(Metadata metadata) { 1338 Bundle bundle = new Bundle(); 1339 bundle.putString(RECORD_ID, metadata.getId()); 1340 bundle.putString(PACKAGE_NAME, metadata.getDataOrigin().getPackageName()); 1341 bundle.putString(CLIENT_ID, metadata.getClientRecordId()); 1342 bundle.putBundle(DEVICE, fromDevice(metadata.getDevice())); 1343 return bundle; 1344 } 1345 fromDevice(Device device)1346 private static Bundle fromDevice(Device device) { 1347 Bundle bundle = new Bundle(); 1348 bundle.putString(MANUFACTURER, device.getManufacturer()); 1349 bundle.putString(MODEL, device.getModel()); 1350 bundle.putInt(DEVICE_TYPE, device.getType()); 1351 return bundle; 1352 } 1353 toMetadata(Bundle bundle)1354 private static Metadata toMetadata(Bundle bundle) { 1355 Metadata.Builder metadata = new Metadata.Builder(); 1356 1357 ifNotNull(bundle.getString(RECORD_ID), metadata::setId); 1358 ifNotNull( 1359 bundle.getString(PACKAGE_NAME), 1360 packageName -> 1361 metadata.setDataOrigin( 1362 new DataOrigin.Builder().setPackageName(packageName).build())); 1363 metadata.setClientRecordId(bundle.getString(CLIENT_ID)); 1364 1365 Bundle deviceBundle = bundle.getBundle(DEVICE); 1366 ifNotNull( 1367 deviceBundle, 1368 nonNullDeviceBundle -> { 1369 Device.Builder deviceBuilder = new Device.Builder(); 1370 ifNotNull( 1371 nonNullDeviceBundle.getString(MANUFACTURER), 1372 deviceBuilder::setManufacturer); 1373 ifNotNull(nonNullDeviceBundle.getString(MODEL), deviceBuilder::setModel); 1374 deviceBuilder.setType( 1375 nonNullDeviceBundle.getInt(DEVICE_TYPE, Device.DEVICE_TYPE_UNKNOWN)); 1376 metadata.setDevice(deviceBuilder.build()); 1377 }); 1378 1379 return metadata.build(); 1380 } 1381 ifNotNull(T obj, Consumer<T> consumer)1382 private static <T> void ifNotNull(T obj, Consumer<T> consumer) { 1383 if (obj == null) { 1384 return; 1385 } 1386 consumer.accept(obj); 1387 } 1388 transformOrNull(T obj, Function<T, R> transform)1389 private static <T, R> R transformOrNull(T obj, Function<T, R> transform) { 1390 if (obj == null) { 1391 return null; 1392 } 1393 return transform.apply(obj); 1394 } 1395 recordClassForName(String className)1396 private static Class<? extends Record> recordClassForName(String className) { 1397 try { 1398 return (Class<? extends Record>) Class.forName(className); 1399 } catch (ClassNotFoundException e) { 1400 throw new IllegalArgumentException(e); 1401 } 1402 } 1403 1404 /** 1405 * Calls {@code ExerciseSessionRecord.Builder.setHasRoute} using reflection as the method is 1406 * hidden. 1407 */ setHasRoute(ExerciseSessionRecord.Builder record, boolean hasRoute)1408 private static void setHasRoute(ExerciseSessionRecord.Builder record, boolean hasRoute) { 1409 // Getting a hidden method by its signature using getMethod() throws an exception in test 1410 // apps, but iterating throw all the methods and getting the needed one works. 1411 for (var method : record.getClass().getMethods()) { 1412 if (method.getName().equals("setHasRoute")) { 1413 try { 1414 method.invoke(record, hasRoute); 1415 } catch (IllegalAccessException | InvocationTargetException e) { 1416 throw new IllegalArgumentException(e); 1417 } 1418 } 1419 } 1420 } 1421 BundleHelper()1422 private BundleHelper() {} 1423 } 1424