Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Health] Adding UV Index Support for Apple HealthKit #1041

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 102 additions & 3 deletions packages/health/ios/Classes/SwiftHealthPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
let GENDER = "GENDER"
let BLOOD_TYPE = "BLOOD_TYPE"
let MENSTRUATION_FLOW = "MENSTRUATION_FLOW"

let UV_EXPOSURE = "UV_EXPOSURE"

// Health Unit types
// MOLE_UNIT_WITH_MOLAR_MASS, // requires molar mass input - not supported yet
Expand Down Expand Up @@ -272,6 +272,16 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
else if (call.method.elementsEqual("writeMeal")){
try! writeMeal(call: call, result: result)
}

/// Handle writeUVExposure
else if (call.method.elementsEqual("writeUVExposure")){
try! writeUVExposure(call: call, result: result)
}

/// Handle writeBatchUVExposure
else if (call.method.elementsEqual("writeBatchUVExposure")){
try! writeBatchUVExposure(call: call, result: result)
}

/// Handle writeInsulinDelivery
else if (call.method.elementsEqual("writeInsulinDelivery")){
Expand Down Expand Up @@ -367,18 +377,21 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
else {
throw PluginError(message: "Invalid Arguments!")
}

print("Eze log :\(types).\(permissions)")

var typesToRead = Set<HKObjectType>()
var typesToWrite = Set<HKSampleType>()
for (index, key) in types.enumerated() {
if (key == NUTRITION) {

for nutritionType in nutritionList {
let nutritionData = dataTypeLookUp(key: nutritionType)
typesToWrite.insert(nutritionData)
}
} else {
let dataType = dataTypeLookUp(key: key)
let access = permissions[index]
print("Eze log : we're not in nutrition \(dataTypeLookUp(key: key)).\(access)")
switch access {
case 0:
typesToRead.insert(dataType)
Expand Down Expand Up @@ -554,6 +567,89 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
}
})
}


func writeUVExposure(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
guard let arguments = call.arguments as? NSDictionary,
let value = (arguments["value"] as? Double),
let startTime = (arguments["startTime"] as? NSNumber),
let endTime = (arguments["endTime"] as? NSNumber),
let recordingMethod = (arguments["recordingMethod"] as? Int)
else {
throw PluginError(message: "Invalid Arguments")
}

let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000)
let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000)

let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue
let metadata: [String: Any] = [
HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)
]

let quantity = HKQuantity(unit: HKUnit.count(), doubleValue: value)
let sample = HKQuantitySample(
type: HKQuantityType.quantityType(forIdentifier: .uvExposure)!,
quantity: quantity, start: dateFrom, end: dateTo, metadata: metadata)

HKHealthStore().save(sample, withCompletion: { (success, error) in
if let err = error {
print("Error Saving UV Exposure Sample: \(err.localizedDescription)")
}
DispatchQueue.main.async {
result(success)
}
})
}

func writeBatchUVExposure(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
guard let arguments = call.arguments as? NSDictionary,
let samplesData = arguments["samples"] as? [[String: Any]] else {
throw PluginError(message: "Invalid Arguments")
}

var samplesToSave: [HKQuantitySample] = []

for sampleData in samplesData {
guard let value = sampleData["value"] as? Double,
let startTime = sampleData["startTime"] as? NSNumber,
let endTime = sampleData["endTime"] as? NSNumber,
let recordingMethod = sampleData["recordingMethod"] as? Int else {
throw PluginError(message: "Invalid Sample Data")
}

let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000)
let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000)
let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue

let metadata: [String: Any] = [
HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)
]

let quantity = HKQuantity(unit: HKUnit.count(), doubleValue: value)
if let uvExposureType = HKQuantityType.quantityType(forIdentifier: .uvExposure) {
let sample = HKQuantitySample(
type: uvExposureType,
quantity: quantity,
start: dateFrom,
end: dateTo,
metadata: metadata)
samplesToSave.append(sample)
} else {
throw PluginError(message: "UV Exposure Type Not Available")
}
}

// Save all samples in one operation
HKHealthStore().save(samplesToSave) { (success, error) in
if let err = error {
print("Error Saving UV Exposure Samples: \(err.localizedDescription)")
}
DispatchQueue.main.async {
result(success)
}
}
}

func writeMeal(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
guard let arguments = call.arguments as? NSDictionary,
Expand Down Expand Up @@ -1298,6 +1394,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
unitDict[MILLIGRAM_PER_DECILITER] = HKUnit.init(from: "mg/dL")
unitDict[UNKNOWN_UNIT] = HKUnit.init(from: "")
unitDict[NO_UNIT] = HKUnit.init(from: "")
unitDict[UV_EXPOSURE] = HKUnit.count()

// Initialize workout types
workoutActivityTypeMap["ARCHERY"] = .archery
Expand Down Expand Up @@ -1483,7 +1580,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
dataTypesDict[SLEEP_REM] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
dataTypesDict[SLEEP_ASLEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
dataTypesDict[MENSTRUATION_FLOW] = HKSampleType.categoryType(forIdentifier: .menstrualFlow)!


dataTypesDict[UV_EXPOSURE] = HKSampleType.quantityType(forIdentifier: .uvExposure)!

dataTypesDict[EXERCISE_TIME] = HKSampleType.quantityType(forIdentifier: .appleExerciseTime)!
dataTypesDict[WORKOUT] = HKSampleType.workoutType()
Expand All @@ -1509,6 +1607,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
dataQuantityTypesDict[BODY_FAT_PERCENTAGE] = HKQuantityType.quantityType(forIdentifier: .bodyFatPercentage)!
dataQuantityTypesDict[BODY_MASS_INDEX] = HKQuantityType.quantityType(forIdentifier: .bodyMassIndex)!
dataQuantityTypesDict[BODY_TEMPERATURE] = HKQuantityType.quantityType(forIdentifier: .bodyTemperature)!
dataQuantityTypesDict[UV_EXPOSURE] = HKQuantityType.quantityType(forIdentifier: .uvExposure)!

// Nutrition
dataQuantityTypesDict[DIETARY_CARBS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryCarbohydrates)!
Expand Down
1 change: 1 addition & 0 deletions packages/health/lib/health.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:flutter/services.dart';
part 'src/heath_data_types.dart';
part 'src/functions.dart';
part 'src/health_data_point.dart';
part 'src/uv_exposure_model.dart';
part 'src/health_value_types.dart';
part 'src/health_plugin.dart';
part 'src/workout_summary.dart';
Expand Down
2 changes: 2 additions & 0 deletions packages/health/lib/health.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 73 additions & 1 deletion packages/health/lib/src/health_plugin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,78 @@ class Health {
return success ?? false;
}

/// Saves an UV Exposure record
///
/// Returns true if successful, false otherwise.
///
/// Parameters:
/// * [uvIndex] - the UV Index to record
/// * [startTime] - the start time when this [value] is measured.
/// Must be equal to or earlier than [endTime].
/// * [endTime] - the end time when this [value] is measured.
/// Must be equal to or later than [startTime].
/// Simply set [endTime] equal to [startTime] if the UV Record is measured
/// only at a specific point in time. If omitted, [endTime] is set to [startTime].
/// * [recordingMethod] - the recording method of the data point.
Future<bool> writeUVExposureData(
{required double uvIndex,
required DateTime startTime,
required DateTime endTime,
RecordingMethod recordingMethod = RecordingMethod.automatic}) async {
if (Platform.isAndroid) {
throw UnsupportedError("writeUVExposureData is not supported on Android");
}

Map<String, dynamic> args = {
'value': uvIndex,
'recordingMethod': recordingMethod.toInt(),
'startTime': startTime.millisecondsSinceEpoch,
'endTime': endTime.millisecondsSinceEpoch
};

bool? success = await _channel.invokeMethod('writeUVExposure', args);
return success ?? false;
}

/// Saves a batch of UV Exposure records
///
/// Returns true if successful, false otherwise.
///
/// Parameters:
/// * samples is a Map
/// With the format
/// final List<UvExposureModel> samples = [
// UvExposureModel(value:1.0,
// startTime: DateTime.now().substract(Duration(minutes:5),
// endTime: DateTime.now(),
// recordingMethod: RecordingMethod.manual),
// // Add more samples as needed
// ];
Future<bool> writeBatchUVExposureData(
{required List<UvExposureModel> samples}) async {
if (Platform.isAndroid) {
throw UnsupportedError("writeUVExposureData is not supported on Android");
}

List<Map<String, dynamic>> samplesMap = samples.map((e) => e.toMap()).toList();
bool success = false;

try {
success = await _channel.invokeMethod('writeBatchUVExposure', {
'samples': samplesMap,
}) ??
false;
if (success) {
print('Samples saved successfully.');
} else {
print('Failed to save samples.');
}
} catch (e) {
print('Error saving samples: $e');
}
return success;
}

/// Saves a blood pressure record.
///
/// Returns true if successful, false otherwise.
Expand Down Expand Up @@ -681,7 +753,7 @@ class Health {
}

var value =
Platform.isAndroid ? MenstrualFlow.toHealthConnect(flow) : flow.index;
Platform.isAndroid ? MenstrualFlow.toHealthConnect(flow) : flow.index;

if (value == -1) {
throw ArgumentError(
Expand Down
5 changes: 5 additions & 0 deletions packages/health/lib/src/heath_data_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ enum HealthDataType {
HEADACHE_SEVERE,
HEADACHE_UNSPECIFIED,
NUTRITION,
UV_EXPOSURE,
UV_EXPOSURE_BATCH,
// HealthKit Characteristics
GENDER,
BIRTH_DATE,
Expand Down Expand Up @@ -206,6 +208,8 @@ const List<HealthDataType> dataTypeKeysIOS = [
HealthDataType.BIRTH_DATE,
HealthDataType.BLOOD_TYPE,
HealthDataType.MENSTRUATION_FLOW,
HealthDataType.UV_EXPOSURE,
HealthDataType.UV_EXPOSURE_BATCH,
];

/// List of data types available on Android
Expand Down Expand Up @@ -352,6 +356,7 @@ const Map<HealthDataType, HealthDataUnit> dataTypeToUnit = {

HealthDataType.NUTRITION: HealthDataUnit.NO_UNIT,
HealthDataType.MENSTRUATION_FLOW: HealthDataUnit.NO_UNIT,
HealthDataType.UV_EXPOSURE: HealthDataUnit.COUNT,

// Health Connect
HealthDataType.TOTAL_CALORIES_BURNED: HealthDataUnit.KILOCALORIE,
Expand Down
33 changes: 33 additions & 0 deletions packages/health/lib/src/uv_exposure_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
part of '../health.dart';

class UvExposureModel {
final double value;
final int startTime;
final int endTime;
final RecordingMethod recordingMethod;

UvExposureModel({
required this.value,
required this.startTime,
required this.endTime,
required this.recordingMethod,
});

factory UvExposureModel.fromMap(Map<String, dynamic> map) {
return UvExposureModel(
value: map['value'] as double,
startTime: map['startTime'] as int,
endTime: map['endTime'] as int,
recordingMethod: RecordingMethod.fromInt(map['recordingMethod'] as int),
);
}

Map<String, dynamic> toMap() {
return {
'value': value,
'startTime': startTime,
'endTime': endTime,
'recordingMethod': recordingMethod.index,
};
}
}