UNPKG

cordova-plugin-health

Version:

A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Health Connect

853 lines (811 loc) 33.9 kB
var Health = function () { this.name = 'health'; }; var dataTypes = []; dataTypes['mindfulness'] = 'HKCategoryTypeIdentifierMindfulSession'; dataTypes['steps'] = 'HKQuantityTypeIdentifierStepCount'; dataTypes['stairs'] = 'HKQuantityTypeIdentifierFlightsClimbed'; dataTypes['distance'] = 'HKQuantityTypeIdentifierDistanceWalkingRunning'; // and HKQuantityTypeIdentifierDistanceCycling dataTypes['calories'] = 'HKQuantityTypeIdentifierActiveEnergyBurned'; // and HKQuantityTypeIdentifierBasalEnergyBurned dataTypes['calories.active'] = 'HKQuantityTypeIdentifierActiveEnergyBurned'; dataTypes['calories.basal'] = 'HKQuantityTypeIdentifierBasalEnergyBurned'; dataTypes['height'] = 'HKQuantityTypeIdentifierHeight'; dataTypes['weight'] = 'HKQuantityTypeIdentifierBodyMass'; dataTypes['bmi'] = 'HKQuantityTypeIdentifierBodyMassIndex'; dataTypes['heart_rate'] = 'HKQuantityTypeIdentifierHeartRate'; dataTypes['workout_route'] = 'HKWorkoutRouteType'; dataTypes['heart_rate.resting'] = 'HKQuantityTypeIdentifierRestingHeartRate'; dataTypes['heart_rate.variability'] = 'HKQuantityTypeIdentifierHeartRateVariabilitySDNN'; dataTypes['fat_percentage'] = 'HKQuantityTypeIdentifierBodyFatPercentage'; dataTypes['waist_circumference'] = 'HKQuantityTypeIdentifierWaistCircumference'; dataTypes['activity'] = 'HKWorkoutTypeIdentifier'; dataTypes['sleep'] = 'HKCategoryTypeIdentifierSleepAnalysis'; dataTypes['nutrition'] = 'HKCorrelationTypeIdentifierFood'; dataTypes['nutrition.calories'] = 'HKQuantityTypeIdentifierDietaryEnergyConsumed'; dataTypes['nutrition.fat.total'] = 'HKQuantityTypeIdentifierDietaryFatTotal'; dataTypes['nutrition.fat.saturated'] = 'HKQuantityTypeIdentifierDietaryFatSaturated'; dataTypes['nutrition.fat.polyunsaturated'] = 'HKQuantityTypeIdentifierDietaryFatPolyunsaturated'; dataTypes['nutrition.fat.monounsaturated'] = 'HKQuantityTypeIdentifierDietaryFatMonounsaturated'; dataTypes['nutrition.cholesterol'] = 'HKQuantityTypeIdentifierDietaryCholesterol'; dataTypes['nutrition.sodium'] = 'HKQuantityTypeIdentifierDietarySodium'; dataTypes['nutrition.potassium'] = 'HKQuantityTypeIdentifierDietaryPotassium'; dataTypes['nutrition.carbs.total'] = 'HKQuantityTypeIdentifierDietaryCarbohydrates'; dataTypes['nutrition.dietary_fiber'] = 'HKQuantityTypeIdentifierDietaryFiber'; dataTypes['nutrition.sugar'] = 'HKQuantityTypeIdentifierDietarySugar'; dataTypes['nutrition.protein'] = 'HKQuantityTypeIdentifierDietaryProtein'; dataTypes['nutrition.vitamin_a'] = 'HKQuantityTypeIdentifierDietaryVitaminA'; dataTypes['nutrition.vitamin_c'] = 'HKQuantityTypeIdentifierDietaryVitaminC'; dataTypes['nutrition.calcium'] = 'HKQuantityTypeIdentifierDietaryCalcium'; dataTypes['nutrition.iron'] = 'HKQuantityTypeIdentifierDietaryIron'; dataTypes['nutrition.water'] = 'HKQuantityTypeIdentifierDietaryWater'; dataTypes['nutrition.caffeine'] = 'HKQuantityTypeIdentifierDietaryCaffeine'; dataTypes['blood_glucose'] = 'HKQuantityTypeIdentifierBloodGlucose'; dataTypes['insulin'] = 'HKQuantityTypeIdentifierInsulinDelivery'; dataTypes['appleExerciseTime'] = 'HKQuantityTypeIdentifierAppleExerciseTime'; dataTypes['blood_pressure'] = 'HKCorrelationTypeIdentifierBloodPressure'; // when requesting auth it's HKQuantityTypeIdentifierBloodPressureSystolic and HKQuantityTypeIdentifierBloodPressureDiastolic dataTypes['blood_pressure_systolic'] = 'HKQuantityTypeIdentifierBloodPressureSystolic'; dataTypes['blood_pressure_diastolic'] = 'HKQuantityTypeIdentifierBloodPressureDiastolic'; dataTypes['resp_rate'] = 'HKQuantityTypeIdentifierRespiratoryRate'; dataTypes['oxygen_saturation'] = 'HKQuantityTypeIdentifierOxygenSaturation'; dataTypes['vo2max'] = 'HKQuantityTypeIdentifierVO2Max'; dataTypes['temperature'] = 'HKQuantityTypeIdentifierBodyTemperature'; dataTypes['UVexposure'] = 'HKQuantityTypeIdentifierUVExposure'; // for parseable units in HK, see https://developer.apple.com/documentation/healthkit/hkunit/1615733-unitfromstring?language=objc var units = []; units['steps'] = 'count'; units['distance'] = 'm'; units['calories'] = 'kcal'; units['calories.active'] = 'kcal'; units['calories.basal'] = 'kcal'; units['height'] = 'm'; units['weight'] = 'kg'; units['bmi'] = 'count'; units['heart_rate'] = 'count/min'; units['heart_rate.resting'] = 'count/min'; units['heart_rate.variability'] = 'ms'; units['fat_percentage'] = '%'; units['waist_circumference'] = 'm'; units['nutrition'] = ['g', 'ml', 'kcal']; units['nutrition.calories'] = 'kcal'; units['nutrition.fat.total'] = 'g'; units['nutrition.fat.saturated'] = 'g'; units['nutrition.fat.polyunsaturated'] = 'g'; units['nutrition.fat.monounsaturated'] = 'g'; units['nutrition.cholesterol'] = 'mg'; units['nutrition.sodium'] = 'mg'; units['nutrition.potassium'] = 'mg'; units['nutrition.carbs.total'] = 'g'; units['nutrition.dietary_fiber'] = 'g'; units['nutrition.sugar'] = 'g'; units['nutrition.protein'] = 'g'; units['nutrition.vitamin_a'] = 'mcg'; units['nutrition.vitamin_c'] = 'mg'; units['nutrition.calcium'] = 'mg'; units['nutrition.iron'] = 'mg'; units['nutrition.water'] = 'ml'; units['nutrition.caffeine'] = 'g'; units['blood_glucose'] = 'mmol/L'; units['insulin'] = 'IU'; units['appleExerciseTime'] = 'min'; units['blood_pressure'] = 'mmHg'; units['blood_pressure_systolic'] = 'mmHg'; units['blood_pressure_diastolic'] = 'mmHg'; units['resp_rate'] = 'count/min'; units['oxygen_saturation'] = '%'; units['vo2max'] = 'ml/(kg*min)'; units['temperature'] = 'degC'; units['UVexposure'] = 'count'; // just a wrapper for querying Telerik's if HK is available Health.prototype.isAvailable = function (success, error) { window.plugins.healthkit.available(success, error); }; Health.prototype.openHealthSettings = function (success, error) { window.plugins.healthkit.openHealthSettings(success, error); }; // returns the equivalent native HealthKit data type from the custom one var getHKDataTypes = function (dtArr) { var HKDataTypes = []; for (var i = 0; i < dtArr.length; i++) { if ((dtArr[i] !== 'gender') && (dtArr[i] !== 'date_of_birth')) { // ignore gender and DOB if (dtArr[i] === 'nutrition') { // add all nutrition stuff for (var dataType in dataTypes) { if (dataType.startsWith('nutrition.')) HKDataTypes.push(dataTypes[dataType]); } } else if (dtArr[i] === 'blood_pressure') { HKDataTypes.push('HKQuantityTypeIdentifierBloodPressureSystolic'); HKDataTypes.push('HKQuantityTypeIdentifierBloodPressureDiastolic'); } else if (dataTypes[dtArr[i]]) { HKDataTypes.push(dataTypes[dtArr[i]]); if (dtArr[i] === 'distance') HKDataTypes.push('HKQuantityTypeIdentifierDistanceCycling'); if (dtArr[i] === 'calories') HKDataTypes.push('HKQuantityTypeIdentifierBasalEnergyBurned'); } else { // return the not found dataType instead of array return dtArr[i]; } } } return HKDataTypes; }; var getReadWriteTypes = function (dts, success, error) { let readTypes = []; let writeTypes = []; let HKDataTypes = []; if (dts.read) { HKDataTypes = getHKDataTypes(dts.read); if (Array.isArray(HKDataTypes)) { readTypes = readTypes.concat(HKDataTypes); } else { error('unknown read data type - ' + HKDataTypes); return; } } if (dts.write) { HKDataTypes = getHKDataTypes(dts.write); if (Array.isArray(HKDataTypes)) { writeTypes = writeTypes.concat(HKDataTypes); } else { error('unknown write data type - ' + HKDataTypes); return; } } success(dedupe(readTypes), dedupe(writeTypes)); }; // requests authorization to HK, a wrapper on top of Telerik's plugin Health.prototype.requestAuthorization = function (dts, onSuccess, onError) { getReadWriteTypes(dts, function (readTypes, writeTypes) { window.plugins.healthkit.requestAuthorization({ 'readTypes': readTypes, 'writeTypes': writeTypes }, onSuccess, onError); }, onError); }; // checks if a datatype has been authorized Health.prototype.isAuthorized = function (dts, onSuccess, onError) { getReadWriteTypes(dts, function (readTypes, writeTypes) { var HKDataTypes = dedupe(readTypes.concat(writeTypes)); var check = function () { if (HKDataTypes.length > 0) { var dt = HKDataTypes.shift(); window.plugins.healthkit.checkAuthStatus({ type: dt }, function (auth) { if (auth === 'authorized') check(); else onSuccess(false); }, onError); } else onSuccess(true); }; check(); }, onError); }; let addWorkoutRoutesAndHeartRate = async function addWorkoutRoutesAndHeartRate(opts, workouts){ for(let i= 0; i < workouts.length; i++){ if(opts.includeRoute) { try { console.log('Querying route for workout', workouts[i].id); let route = await new Promise((resolve, reject) => { window.plugins.healthkit.queryWorkoutRoutes(workouts[i].id, resolve, reject); }); console.log('Route for workout', workouts[i].id, route); route.forEach((point) => { point.timestamp = new Date(point.timestamp); }); workouts[i].route = route; } catch (e) { console.log('Error querying route during workout', e); } } if(opts.includeHeartRate) { try { let heartRate = await new Promise((resolve, reject) => { window.plugins.healthkit.queryHeartRateDuringWorkout(workouts[i].id, resolve, reject); }); heartRate.forEach((point) => { point.timestamp = new Date(point.timestamp); }); workouts[i].heartRate = heartRate; } catch (e) { console.log('Error querying heart rate during workout', e); } } } } // queries for a datatype Health.prototype.query = function (opts, onSuccess, onError) { var startD = opts.startDate; var endD = opts.endDate; if (opts.dataType === 'gender') { window.plugins.healthkit.readGender(function (data) { var res = []; res[0] = { startDate: startD, endDate: endD, value: data, sourceName: 'Health', sourceBundleId: 'com.apple.Health' }; onSuccess(res); }, onError); } else if (opts.dataType === 'date_of_birth') { window.plugins.healthkit.readDateOfBirth(function (data) { data.startDate = startD; data.endDate = endD; var res = []; var date = new Date(data); res[0] = { startDate: opts.startDate, endDate: opts.endDate, value: { day: date.getDate(), month: date.getMonth() + 1, year: date.getFullYear() }, sourceName: 'Health', sourceBundleId: 'com.apple.Health' }; onSuccess(res); }, onError); } else if (opts.dataType === 'activity') { // opts is not really used, Telerik's plugin just returns ALL workouts window.plugins.healthkit.findWorkouts(opts, function (data) { let result = []; for (let i = 0; i < data.length; i++) { let res = {}; res.id = data[i].UUID res.startDate = new Date(data[i].startDate); res.endDate = new Date(data[i].endDate); // filter the results based on the dates if ((res.startDate >= opts.startDate) && (res.endDate <= opts.endDate)) { res.value = data[i].activityType; res.unit = 'activityType'; if (data[i].energy) res.calories = parseInt(data[i].energy); if (data[i].distance) res.distance = parseInt(data[i].distance); if (data[i].duration) res.duration = data[i].duration; res.sourceName = data[i].sourceName; res.sourceBundleId = data[i].sourceBundleId; result.push(res); } } if(opts.includeRoute || opts.includeHeartRate) { addWorkoutRoutesAndHeartRate(opts, result) .then(() => onSuccess(result), onError); } else { onSuccess(result); } }, onError); } else if (opts.dataType === 'nutrition' || opts.dataType === 'blood_pressure') { // do the correlation queries var result = []; var qops = { // query-specific options startDate: opts.startDate, endDate: opts.endDate, correlationType: dataTypes[opts.dataType] } if (units[opts.dataType].constructor.name == "Array") qops.units = units[opts.dataType]; else qops.units = [units[opts.dataType]]; window.plugins.healthkit.queryCorrelationType(qops, function (data) { for (var i = 0; i < data.length; i++) { result.push(prepareCorrelation(data[i], opts.dataType)); } onSuccess(result); }, onError); } else if (dataTypes[opts.dataType]) { opts.sampleType = dataTypes[opts.dataType]; if (units[opts.dataType]) { opts.unit = units[opts.dataType]; } window.plugins.healthkit.querySampleType(opts, function (data) { let result = []; let convertSamples = function (samples) { for (let i = 0; i < samples.length; i++) { let res = {}; res.id = samples[i].UUID res.startDate = new Date(samples[i].startDate); res.endDate = new Date(samples[i].endDate); if (opts.dataType === 'blood_glucose') { res.value = { glucose: samples[i].quantity } if (samples[i].metadata && samples[i].metadata.HKBloodGlucoseMealTime) { if (samples[i].metadata.HKBloodGlucoseMealTime == 1) res.value.meal = 'before_meal' else res.value.meal = 'after_meal' } if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime) res.value.meal = samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime; // overwrite HKBloodGlucoseMealTime if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseSleepTime) res.value.sleep = samples[i].metadata.HKMetadataKeyBloodGlucoseSleepTime; if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseSource) res.value.source = samples[i].metadata.HKMetadataKeyBloodGlucoseSource; } else if (opts.dataType === 'insulin') { res.value = { insulin: samples[i].quantity } if (samples[i].metadata && samples[i].metadata.HKInsulinDeliveryReason) { if (samples[i].metadata.HKInsulinDeliveryReason == 1) res.value.reason = 'basal' else res.value.reason = 'bolus' } if (samples[i].metadata && samples[i].metadata.HKMetadataKeyInsulinDeliveryReason) res.value.reason = samples[i].metadata.HKMetadataKeyInsulinDeliveryReason; // overwrite HKInsulinDeliveryReason } else if (opts.dataType === 'sleep') { switch (data[i].value) { case 0: res.value = 'sleep.inBed'; break; case 1: default: res.value = 'sleep'; break; case 2: res.value = 'sleep.awake'; break; case 3: res.value = 'sleep.light'; break; case 4: res.value = 'sleep.deep'; break; case 5: res.value = 'sleep.rem'; break; } res.unit = 'sleep'; } else { res.value = samples[i].quantity; } if (samples[i].unit) res.unit = samples[i].unit; else if (opts.unit) res.unit = opts.unit; res.sourceName = samples[i].sourceName; res.sourceBundleId = samples[i].sourceBundleId; result.push(res); } }; convertSamples(data); if (opts.dataType === 'distance') { // in the case of the distance, add the cycling distances opts.sampleType = 'HKQuantityTypeIdentifierDistanceCycling'; // re-assign start and end times (because the plugin modifies them later) opts.startDate = startD; opts.endDate = endD; window.plugins.healthkit.querySampleType(opts, function (data) { convertSamples(data); onSuccess(result); }, onError); } else if (opts.dataType === 'calories') { // in the case of the calories, add the basal opts.sampleType = 'HKQuantityTypeIdentifierBasalEnergyBurned'; opts.startDate = startD; opts.endDate = endD; window.plugins.healthkit.querySampleType(opts, function (data) { convertSamples(data); onSuccess(result); }, onError); } else onSuccess(result); }, onError); // first call to querySampleType } else { onError('unknown data type ' + opts.dataType); } }; Health.prototype.queryAggregated = function (opts, onSuccess, onError) { if ((opts.dataType !== 'steps') && (opts.dataType !== 'distance') && (opts.dataType !== 'calories') && (opts.dataType !== 'calories.active') && (opts.dataType !== 'calories.basal') && (opts.dataType !== 'activity') && (opts.dataType !== 'heart_rate') && (opts.dataType !== 'heart_rate.resting') && (opts.dataType !== 'heart_rate.variability') && (opts.dataType !== 'weight') && (!opts.dataType.startsWith('nutrition')) && (opts.dataType !== 'appleExerciseTime')) { // unsupported datatype onError('Datatype ' + opts.dataType + ' not supported in queryAggregated'); return; } var startD = opts.startDate; var endD = opts.endDate; opts.sampleType = dataTypes[opts.dataType]; if (units[opts.dataType]) opts.unit = units[opts.dataType]; if (opts.bucket) { // ----- with buckets opts.aggregation = opts.bucket; if (opts.dataType === 'activity') { // query and manually aggregate cordova.plugins.health.query(opts, function (data) { onSuccess(bucketize(data, opts.bucket, startD, endD, 'activitySummary', mergeActivitySamples)); }, onError); } else if (opts.dataType === 'nutrition') { // query and manually aggregate cordova.plugins.health.query(opts, function (data) { onSuccess(bucketize(data, opts.bucket, startD, endD, 'nutrition', mergeNutritionSamples)); }, onError); } else { window.plugins.healthkit.querySampleTypeAggregated(opts, function (value) { if (opts.dataType === 'distance') { // add cycled distance let rundists = value; opts.sampleType = 'HKQuantityTypeIdentifierDistanceCycling'; opts.startDate = startD; opts.endDate = endD; window.plugins.healthkit.querySampleTypeAggregated(opts, function (dists) { onSuccess(prepareResults(rundists, opts.unit, dists)); }, onError); } else if (opts.dataType === 'calories') { // add basal calories let activecals = value; opts.sampleType = 'HKQuantityTypeIdentifierBasalEnergyBurned'; opts.startDate = startD; opts.endDate = endD; window.plugins.healthkit.querySampleTypeAggregated(opts, function (cals) { onSuccess(prepareResults(activecals, opts.unit, cals)); }, onError); } else { //simply refactor the result and send it onSuccess(prepareResults(value, opts.unit)); } }, onError); } } else { // ---- no bucketing, just sum if (opts.dataType === 'activity') { cordova.plugins.health.query(opts, function (data) { // manually aggregate by activity onSuccess(aggregateIntoResult(data, 'activitySummary', mergeActivitySamples)); }, onError); } else if (opts.dataType === 'nutrition') { // manually aggregate by nutrition cordova.plugins.health.query(opts, function (data) { onSuccess(aggregateIntoResult(data, 'nutrition', mergeNutritionSamples)); }, onError); } else { window.plugins.healthkit.sumQuantityType(opts, function (value) { if (opts.dataType === 'distance') { // add cycled distance let dist = value; opts.sampleType = 'HKQuantityTypeIdentifierDistanceCycling'; opts.startDate = startD; opts.endDate = endD; window.plugins.healthkit.sumQuantityType(opts, function (value) { onSuccess({ startDate: startD, endDate: endD, value: value + dist, unit: opts.unit }); }, onError); } else if (opts.dataType === 'calories') { // add basal calories let activecals = value; opts.sampleType = 'HKQuantityTypeIdentifierBasalEnergyBurned'; opts.startDate = startD; opts.endDate = endD; window.plugins.healthkit.sumQuantityType(opts, function (basalcals) { onSuccess({ startDate: startD, endDate: endD, value: basalcals + activecals, unit: opts.unit }); }, onError); } else { onSuccess({ startDate: startD, endDate: endD, value: value, unit: opts.unit }); } }, onError); } } }; Health.prototype.store = function (data, onSuccess, onError) { if (data.dataType === 'gender') { onError('Gender is not writeable'); } else if (data.dataType === 'date_of_birth') { onError('Date of birth is not writeable'); } else if (data.dataType === 'activity') { // activity / workouts are treated separately data.activityType = data.value; data.requestReadPermission = false // do not ask for read permission too if (data.calories) { data.energy = data.calories; data.energyUnit = 'kcal'; } if (data.distance) { data.distance = data.distance; data.distanceUnit = 'm'; } window.plugins.healthkit.saveWorkout(data, onSuccess, onError); } else if (data.dataType === 'sleep') { // sleep needs to be mapped to a specific value data.sampleType = 'HKCategoryTypeIdentifierSleepAnalysis'; if (data.value === 'sleep') { if (getOSVersion() >= 16) { data.value = 'HKCategoryValueSleepAnalysisAsleepUnspecified'; } else { data.value = 'HKCategoryValueSleepAnalysisAsleep'; } } else if (data.value === 'sleep.inBed') { data.value = 'HKCategoryValueSleepAnalysisInBed'; } else if (data.value === 'sleep.light') { data.value = 'HKCategoryValueSleepAnalysisAsleepCore'; } else if (data.value === 'sleep.deep') { data.value = 'HKCategoryValueSleepAnalysisAsleepDeep'; } else if (data.value === 'sleep.rem') { data.value = 'HKCategoryValueSleepAnalysisAsleepREM'; } else if (data.value === 'sleep.awake') { data.value = 'HKCategoryValueSleepAnalysisAwake'; } else if (data.value === 'sleep.outOfBed') { data.value = 'HKCategoryValueSleepAnalysisAwake'; } window.plugins.healthkit.saveSample(data, onSuccess, onError); } else if (data.dataType === 'nutrition') { data.correlationType = 'HKCorrelationTypeIdentifierFood'; if (!data.metadata) data.metadata = {}; if (data.value.item) data.metadata.HKFoodType = data.value.item; if (data.value.meal_type) data.metadata.HKFoodMeal = data.value.meal_type; if (data.value.brand_name) data.metadata.HKFoodBrandName = data.value.brand_name; data.samples = []; for (let nutrientName in data.value.nutrients) { let unit = units[nutrientName]; let sampletype = dataTypes[nutrientName]; if (!sampletype) { onError('Cannot recognise nutrition item ' + nutrientName); return; } let sample = { 'startDate': data.startDate, 'endDate': data.endDate, 'sampleType': sampletype, 'unit': unit, 'amount': data.value.nutrients[nutrientName] }; data.samples.push(sample) } window.plugins.healthkit.saveCorrelation(data, onSuccess, onError); } else if (data.dataType === 'blood_pressure') { data.correlationType = 'HKCorrelationTypeIdentifierBloodPressure'; data.samples = [{ 'startDate': data.startDate, 'endDate': data.endDate, 'sampleType': 'HKQuantityTypeIdentifierBloodPressureSystolic', 'unit': 'mmHg', 'amount': data.value.systolic }, { 'startDate': data.startDate, 'endDate': data.endDate, 'sampleType': 'HKQuantityTypeIdentifierBloodPressureDiastolic', 'unit': 'mmHg', 'amount': data.value.diastolic }]; window.plugins.healthkit.saveCorrelation(data, onSuccess, onError); } else if (dataTypes[data.dataType]) { // generic case data.sampleType = dataTypes[data.dataType]; if ((data.dataType === 'distance') && data.cycling) { data.sampleType = 'HKQuantityTypeIdentifierDistanceCycling'; } if (data.dataType === 'blood_glucose') { data.amount = data.value.glucose; if (!data.metadata) data.metadata = {}; if (data.value.meal) { data.metadata.HKMetadataKeyBloodGlucoseMealTime = data.value.meal; if (data.value.meal.startsWith('before_')) data.metadata.HKBloodGlucoseMealTime = 1; else if (data.value.meal.startsWith('after_')) data.metadata.HKBloodGlucoseMealTime = 2; } if (data.value.sleep) data.metadata.HKMetadataKeyBloodGlucoseSleepTime = data.value.sleep; if (data.value.source) data.metadata.HKMetadataKeyBloodGlucoseSource = data.value.source; } else if (data.dataType === 'insulin') { data.amount = data.value.insulin; if (!data.metadata) data.metadata = {}; if (data.value.reason) { data.metadata.HKMetadataKeyInsulinDeliveryReason = data.value.reason; if (data.value.reason.toLowerCase() === 'basal') data.metadata.HKInsulinDeliveryReason = 1; else if (data.value.reason.toLowerCase() === 'bolus') data.metadata.HKInsulinDeliveryReason = 2; } } else { data.amount = data.value; } if (units[data.dataType]) { data.unit = units[data.dataType]; } window.plugins.healthkit.saveSample(data, onSuccess, onError); } else { onError('unknown data type ' + data.dataType); } }; Health.prototype.delete = function (data, onSuccess, onError) { if (data.dataType === 'gender') { onError('Gender is not deletable'); } else if (data.dataType === 'date_of_birth') { onError('Date of birth is not deletable'); } else if (data.dataType === 'activity') { data.sampleType = 'workoutType'; } else if ((data.dataType === 'distance') && data.cycling) { data.sampleType = 'HKQuantityTypeIdentifierDistanceCycling'; } else if (dataTypes[data.dataType]) { data.sampleType = dataTypes[data.dataType]; } else { onError('unknown data type ' + data.dataType); return; } window.plugins.healthkit.deleteSamples(data, onSuccess, onError); }; cordova.addConstructor(function () { cordova.plugins.health = new Health(); return cordova.plugins.health; }); // UTILITY functions // get iOS version number getOSVersion = function () { var agent = window.navigator.userAgent, start = agent.indexOf('OS '); if ((agent.indexOf('iPhone') > -1 || agent.indexOf('iPad') > -1) && start > -1) { return window.Number(agent.substr(start + 3, 3).replace('_', '.')); } return 0; } // shallow removal of duplicates in an array var dedupe = function (arr) { return arr.filter(function (el, i, arr) { return arr.indexOf(el) === i; }); }; // converts from grams into another unit // if the unit is not specified or is not weight, then the original quantity is returned var convertFromGrams = function (toUnit, q) { if (toUnit === 'mcg') return q * 1000000; if (toUnit === 'mg') return q * 1000; if (toUnit === 'kg') return q / 1000; return q; } // converts to grams from another unit var convertToGrams = function (fromUnit, q) { if (fromUnit === 'mcg') return q / 1000000; if (fromUnit === 'mg') return q / 1000; if (fromUnit === 'kg') return q * 1000; return q; } // refactors the result of a quantity type query into returned type var prepareResult = function (data, unit) { let res = { id: data.UUID, startDate: new Date(data.startDate), endDate: new Date(data.endDate), value: data.quantity, unit: unit }; if (data.sourceName) res.sourceName = data.sourceName; if (data.sourceBundleId) res.sourceBundleId = data.sourceBundleId; return res; }; // refactors the result of a correlation query into returned type var prepareCorrelation = function (data, dataType) { let res = { id: data.UUID, startDate: new Date(data.startDate), endDate: new Date(data.endDate), value: {} }; if (data.sourceName) res.sourceName = data.sourceName; if (data.sourceBundleId) res.sourceBundleId = data.sourceBundleId; if (dataType === 'nutrition') { res.unit = 'nutrition' if (data.metadata && data.metadata.HKFoodType) res.value.item = data.metadata.HKFoodType; if (data.metadata && data.metadata.HKFoodMeal) res.value.meal_type = data.metadata.HKFoodMeal; if (data.metadata && data.metadata.HKFoodBrandName) res.value.brand_name = data.metadata.HKFoodBrandName; res.value.nutrients = {}; for (let j = 0; j < data.samples.length; j++) { let sample = data.samples[j]; for (let dataname in dataTypes) { if (dataTypes[dataname] === sample.sampleType) { res.value.nutrients[dataname] = convertFromGrams(units[dataname], sample.value); break; } } } } else if (dataType === 'blood_pressure') { res.unit = 'mmHG' for (let j = 0; j < data.samples.length; j++) { let sample = data.samples[j]; if (sample.sampleType === 'HKQuantityTypeIdentifierBloodPressureSystolic') res.value.systolic = sample.value; if (sample.sampleType === 'HKQuantityTypeIdentifierBloodPressureDiastolic') res.value.diastolic = sample.value; } } return res; }; // merges activity (workout) samples // fromObj is formatted as returned by query var mergeActivitySamples = function (fromObj, intoObj) { if (!intoObj.value) intoObj.value = {}; let dur = (fromObj.endDate - fromObj.startDate); let dist = fromObj.distance; let cals = fromObj.calories; if (intoObj.value[fromObj.value]) { intoObj.value[fromObj.value].duration += dur; intoObj.value[fromObj.value].distance += dist; intoObj.value[fromObj.value].calories += cals; } else { intoObj.value[fromObj.value] = { duration: dur, distance: dist, calories: cals }; } }; // merges nutrition samples var mergeNutritionSamples = function (fromObj, intoObj) { if (!intoObj.value) intoObj.value = {}; for (let dataname in fromObj.value.nutrients) { if (!intoObj.value[dataname]) intoObj.value[dataname] = fromObj.value.nutrients[dataname]; else intoObj.value[dataname] += fromObj.value.nutrients[dataname]; } }; // aggregates the results into one var aggregateIntoResult = function (data, unit, merge) { var res = { startDate: new Date(new Date().getTime() + 10 * 365 * 24 * 60 * 60 * 1000), // big date endDate: new Date(new Date().getTime() - 10 * 365 * 24 * 60 * 60 * 1000), // small date value: {}, unit: unit }; for (var i = 0; i < data.length; i++) { if (data[i].endDate > res.endDate) res.endDate = data[i].endDate; if (data[i].startDate < res.startDate) res.startDate = data[i].startDate; merge(data[i], res); } return res; }; // takes the result of a query (data) and transforms them, also merges with (unprocessed) results of another query var prepareResults = function (data, unit, mergeWith) { var retval = []; for (var i = 0; i < data.length; i++) { var retsample = prepareResult(data[i], unit); if (mergeWith) { // merge with existing array returned by a query for (var j = 0; j < mergeWith.length; j++) { // we expect the buckets to have the same start and end dates var mergeSample = prepareResult(mergeWith[j], unit); if (retsample.startDate.getTime() === mergeSample.startDate.getTime()) { retsample.value += mergeSample.value; } } } retval.push(retsample); } return retval; }; // takes the results of a query (data) and merges it into a bucketized result (returned) var bucketize = function (data, bucket, startD, endD, unit, merge) { var retval = []; // create buckets var sd; if (bucket === 'hour') { sd = new Date(startD.getFullYear(), startD.getMonth(), startD.getDate(), startD.getHours()); } else if (bucket === 'day') { sd = new Date(startD.getFullYear(), startD.getMonth(), startD.getDate()); } else if (bucket === 'week') { sd = new Date(startD.getTime()); sd.setDate(startD.getDate() - (startD.getDay() === 0 ? 6 : startD.getDay() - 1)); // last monday } else if (bucket === 'month') { sd = new Date(startD.getFullYear(), startD.getMonth()); } else if (bucket === 'year') { sd = new Date(startD.getFullYear()); } else { throw 'Bucket not recognised ' + bucket; } while (sd <= endD) { var ed; if (bucket === 'hour') { ed = new Date(sd.getFullYear(), sd.getMonth(), sd.getDate(), sd.getHours() + 1); } else if (bucket === 'day') { ed = new Date(sd.getFullYear(), sd.getMonth(), sd.getDate() + 1); } else if (bucket === 'week') { ed = new Date(sd.getFullYear(), sd.getMonth(), sd.getDate() + 7); } else if (bucket === 'month') { ed = new Date(sd.getFullYear(), sd.getMonth() + 1); } else if (bucket === 'year') { ed = new Date(sd.getFullYear() + 1); } retval.push({ startDate: sd, endDate: ed, value: {}, unit: unit }); sd = ed; } for (var i = 0; i < data.length; i++) { // select the bucket for (var j = 0; j < retval.length; j++) { if ((data[i].endDate <= retval[j].endDate) && (data[i].startDate >= retval[j].startDate)) { merge(data[i], retval[j]); } } } return retval; };