• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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