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