From 056530f59df244fe79f5487c486ffe42f5f67df5 Mon Sep 17 00:00:00 2001 From: Gabrielle Earnshaw <3994904+gearnshaw@users.noreply.github.com> Date: Tue, 14 Nov 2023 01:48:58 +0000 Subject: [PATCH] Added the inLocalTimeZone parameter to the prepareResponse function. (#338) * Added the inLocalTimeZone parameter to the prepareResponse function. This defaults to false (existing behaviour) so is backwards compatible. When inLocalTimeZone = false, dates are returned in the format: "endDate":"2022-12-22T16:51:30.000Z" When inLocalTimeZone = true, dates are returned in the format: "endDate":"2022-12-22T11:51:30.000-0500" This allows performance improvements, because you don't have to perform further date processing on the values to convert them back into the local time zone. This PR adds the flag as a parameter to getSleepSamples. It can easily be added to other functions as required. * Added logging to test running in app code * Added the code to retrieve aggregated heart rates. * Fixed call to get health history. * Added extra logging * Fixed reading values and removed logging * Removed more logging. * Fixed issue where all values were overwriting the average. * Fixed a typo. * Added the 'in local timezone' option to aggregated heart rates --- .../googlefit/GoogleFitModule.java | 16 +++++++ .../reactnative/googlefit/HealthHistory.java | 45 +++++++++++++++++++ index.android.d.ts | 18 +++++++- index.android.js | 19 +++++++- src/utils.js | 11 +++-- 5 files changed, 103 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/reactnative/googlefit/GoogleFitModule.java b/android/src/main/java/com/reactnative/googlefit/GoogleFitModule.java index 8c2cfe2c..0a21a025 100644 --- a/android/src/main/java/com/reactnative/googlefit/GoogleFitModule.java +++ b/android/src/main/java/com/reactnative/googlefit/GoogleFitModule.java @@ -466,6 +466,22 @@ public void getHeartRateSamples(double startDate, } } + @ReactMethod + public void getAggregatedHeartRateSamples(double startDate, + double endDate, + int bucketInterval, + String bucketUnit, + Promise promise) { + + try { + HealthHistory healthHistory = mGoogleFitManager.getHealthHistory(); + healthHistory.setDataType(DataType.TYPE_HEART_RATE_BPM); + promise.resolve(healthHistory.getAggregatedHeartRateHistory((long)startDate, (long)endDate, bucketInterval, bucketUnit)); + } catch (IllegalViewOperationException e) { + promise.reject(e); + } + } + @ReactMethod public void getRestingHeartRateSamples(double startDate, double endDate, diff --git a/android/src/main/java/com/reactnative/googlefit/HealthHistory.java b/android/src/main/java/com/reactnative/googlefit/HealthHistory.java index ce87243a..b82ad179 100644 --- a/android/src/main/java/com/reactnative/googlefit/HealthHistory.java +++ b/android/src/main/java/com/reactnative/googlefit/HealthHistory.java @@ -102,6 +102,47 @@ else if (dataReadResult.getDataSets().size() > 0) { return map; } + /** + * GLE added to allow us to aggregate heart rate data. + * It does the same as health history, but adds the aggregation. + * Note there are also some changes to the processDataSet method to allow for the aggregation. + */ + public ReadableArray getAggregatedHeartRateHistory(long startTime, long endTime, int bucketInterval, String bucketUnit) { + DataReadRequest.Builder readRequestBuilder = new DataReadRequest.Builder() + .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS); + + if (this.dataType == DataType.TYPE_HEART_RATE_BPM) { + readRequestBuilder + .aggregate(this.dataType, DataType.AGGREGATE_HEART_RATE_SUMMARY) + .bucketByTime(bucketInterval, HelperUtil.processBucketUnit(bucketUnit)); + } else { + readRequestBuilder.read(this.dataType); + } + + DataReadRequest readRequest = readRequestBuilder.build(); + + DataReadResult dataReadResult = Fitness.HistoryApi.readData(googleFitManager.getGoogleApiClient(), readRequest).await(1, TimeUnit.MINUTES); + + WritableArray map = Arguments.createArray(); + + //Used for aggregated data + if (dataReadResult.getBuckets().size() > 0) { + for (Bucket bucket : dataReadResult.getBuckets()) { + List dataSets = bucket.getDataSets(); + for (DataSet dataSet : dataSets) { + processDataSet(dataSet, map); + } + } + } + //Used for non-aggregated data + else if (dataReadResult.getDataSets().size() > 0) { + for (DataSet dataSet : dataReadResult.getDataSets()) { + processDataSet(dataSet, map); + } + } + return map; + } + public ReadableArray getRestingHeartRateHistory(long startTime, long endTime, int bucketInterval, String bucketUnit) { DataReadRequest.Builder readRequestBuilder = new DataReadRequest.Builder() .aggregate(new DataSource.Builder() @@ -283,6 +324,10 @@ private void processDataSet(DataSet dataSet, WritableArray map) { if (this.dataType == HealthDataTypes.TYPE_BLOOD_PRESSURE) { stepMap.putDouble("diastolic", dp.getValue(HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC).asFloat()); stepMap.putDouble("systolic", dp.getValue(HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC).asFloat()); + } else if (this.dataType == DataType.TYPE_HEART_RATE_BPM && field.toString().startsWith("average")) { + stepMap.putDouble("average", dp.getValue(Field.FIELD_AVERAGE).asFloat()); + stepMap.putDouble("min", dp.getValue(Field.FIELD_MIN).asFloat()); + stepMap.putDouble("max", dp.getValue(Field.FIELD_MAX).asFloat()); } else { stepMap.putDouble("value", dp.getValue(field).asFloat()); } diff --git a/index.android.d.ts b/index.android.d.ts index f555a865..c8d23dec 100644 --- a/index.android.d.ts +++ b/index.android.d.ts @@ -111,6 +111,11 @@ declare module 'react-native-google-fit' { options: StartAndEndDate & Partial ) => Promise; + getAggregatedHeartRateSamples: ( + options: StartAndEndDate & Partial, + inLocalTimeZone: boolean + ) => Promise; + /** * Query for getting resting heart rate samples. the options object is used to setup a query to retrieve relevant samples. * @param {Object} options getRestingHeartRateSamples accepts an options object startDate: ISO8601Timestamp and endDate: ISO8601Timestamp. @@ -181,9 +186,11 @@ declare module 'react-native-google-fit' { /** * Get the sleep sessions over a specified date range. * @param {Object} options getSleepData accepts an options object containing required startDate: ISO8601Timestamp and endDate: ISO8601Timestamp. + * @param inLocalTimeZone return start and end dates in local time zone rather than converting to UTC. */ getSleepSamples: ( - options: Partial + options: Partial, + inLocalTimeZone: boolean ) => Promise saveSleep: ( @@ -340,6 +347,15 @@ declare module 'react-native-google-fit' { wasManuallyEntered: boolean }; + export type AggregatedHeartRateResponse = { + startDate: string, + endDate: string, + min: number, + average: number, + max: number, + day: Day, + } + export type BloodPressureResponse = { startDate: string, endDate: string, diff --git a/index.android.js b/index.android.js index de5eae69..df0f9323 100644 --- a/index.android.js +++ b/index.android.js @@ -567,6 +567,20 @@ class RNGoogleFit { return result; } + getAggregatedHeartRateSamples = async (options, inLocalTimeZone = false) => { + const { startDate, endDate, bucketInterval, bucketUnit } = prepareInput(options); + const result = await googleFit.getAggregatedHeartRateSamples( + startDate, + endDate, + bucketInterval, + bucketUnit + ); + if (result.length > 0) { + return prepareResponse(result, 'average', inLocalTimeZone); + } + return result; + } + getRestingHeartRateSamples = async (options) => { const { startDate, endDate, bucketInterval, bucketUnit } = prepareInput(options); const result = await googleFit.getRestingHeartRateSamples( @@ -689,9 +703,10 @@ class RNGoogleFit { /** * Get the sleep sessions over a specified date range. * @param {Object} options getSleepData accepts an options object containing required startDate: ISO8601Timestamp and endDate: ISO8601Timestamp. + * @param inLocalTimeZone return start and end dates in local time zone rather than converting to UTC */ - getSleepSamples = async (options) => { + getSleepSamples = async (options, inLocalTimeZone = false) => { const { startDate, endDate } = prepareInput(options); const result = await googleFit.getSleepSamples( @@ -699,7 +714,7 @@ class RNGoogleFit { endDate ); - return prepareResponse(result, "addedBy"); + return prepareResponse(result, "addedBy", inLocalTimeZone); } saveSleep = async (options) => { diff --git a/src/utils.js b/src/utils.js index a734ae28..74974ea9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -48,7 +48,7 @@ export function prepareInput(options) { return { startDate, endDate, bucketInterval, bucketUnit }; } -export function prepareResponse(response, byKey = 'value') { +export function prepareResponse(response, byKey = 'value', inLocalTimeZone = false) { return response .map(el => { if (!isNil(el[byKey])) { @@ -57,8 +57,13 @@ export function prepareResponse(response, byKey = 'value') { // the Chrome V8 debugger, but not on device. Using momentJS here to get around this issue. // el.startDate = new Date(el.startDate).toISOString() // el.endDate = new Date(el.endDate).toISOString() - el.startDate = moment(el.startDate).toISOString() - el.endDate = moment(el.endDate).toISOString() + if (inLocalTimeZone) { + el.startDate = moment.parseZone(el.startDate).toISOString(true) + el.endDate = moment.parseZone(el.endDate).toISOString(true) + } else { + el.startDate = moment(el.startDate).toISOString() + el.endDate = moment(el.endDate).toISOString() + } return el } })