Skip to content
Merged
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
5 changes: 5 additions & 0 deletions packages/health/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 13.1.3

* Fix permissions issues with iOS
* Fix [#1231](https://github.com/cph-cachet/flutter-plugins/issues/1231)

## 13.1.2

* Fix [#1250](https://github.com/cph-cachet/flutter-plugins/issues/1250)
Expand Down

Large diffs are not rendered by default.

259 changes: 196 additions & 63 deletions packages/health/example/lib/main.dart

Large diffs are not rendered by default.

158 changes: 91 additions & 67 deletions packages/health/ios/Classes/HealthDataOperations.swift
Original file line number Diff line number Diff line change
@@ -1,84 +1,87 @@
import HealthKit
import Flutter
import HealthKit

/// Class for managing health data permissions and deletion operations
class HealthDataOperations {
let healthStore: HKHealthStore
let dataTypesDict: [String: HKSampleType]
let characteristicsTypesDict: [String: HKCharacteristicType]
let nutritionList: [String]

/// - Parameters:
/// - healthStore: The HealthKit store
/// - dataTypesDict: Dictionary of data types
/// - characteristicsTypesDict: Dictionary of characteristic types
/// - nutritionList: List of nutrition data types
init(healthStore: HKHealthStore,
dataTypesDict: [String: HKSampleType],
characteristicsTypesDict: [String: HKCharacteristicType],
nutritionList: [String]) {
init(
healthStore: HKHealthStore,
dataTypesDict: [String: HKSampleType],
characteristicsTypesDict: [String: HKCharacteristicType],
nutritionList: [String]
) {
self.healthStore = healthStore
self.dataTypesDict = dataTypesDict
self.characteristicsTypesDict = characteristicsTypesDict
self.nutritionList = nutritionList
}

/// Check if HealthKit is available on the device
/// - Parameters:
/// - call: Flutter method call
/// - result: Flutter result callback
func checkIfHealthDataAvailable(call: FlutterMethodCall, result: @escaping FlutterResult) {
result(HKHealthStore.isHealthDataAvailable())
}

/// Check if we have required permissions
/// - Parameters:
/// - call: Flutter method call
/// - result: Flutter result callback
func hasPermissions(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
let arguments = call.arguments as? NSDictionary
guard var types = arguments?["types"] as? [String],
var permissions = arguments?["permissions"] as? [Int],
types.count == permissions.count
var permissions = arguments?["permissions"] as? [Int],
types.count == permissions.count
else {
throw PluginError(message: "Invalid Arguments!")
}

if let nutritionIndex = types.firstIndex(of: HealthConstants.NUTRITION) {
types.remove(at: nutritionIndex)
let nutritionPermission = permissions[nutritionIndex]
permissions.remove(at: nutritionIndex)

for nutritionType in nutritionList {
types.append(nutritionType)
permissions.append(nutritionPermission)
}
}

for (index, type) in types.enumerated() {
guard let sampleType = dataTypesDict[type] else {
print("Warning: Health data type '\(type)' not found in dataTypesDict")
result(false)
return
}

let success = hasPermission(type: sampleType, access: permissions[index])
if success == nil || success == false {
result(success)
return
}
if let characteristicType = characteristicsTypesDict[type] {
let characteristicSuccess = hasPermission(type: characteristicType, access: permissions[index])
if (characteristicSuccess == nil || characteristicSuccess == false) {
let characteristicSuccess = hasPermission(
type: characteristicType, access: permissions[index])
if characteristicSuccess == nil || characteristicSuccess == false {
result(characteristicSuccess)
return
}
}
}

result(true)
}

/// Check if we have permission for a specific type
/// - Parameters:
/// - type: The object type to check
Expand All @@ -99,35 +102,47 @@ class HealthDataOperations {
return nil
}
}

/// Request authorization for health data
/// - Parameters:
/// - call: Flutter method call
/// - result: Flutter result callback
func requestAuthorization(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
guard let arguments = call.arguments as? NSDictionary,
let types = arguments["types"] as? [String],
let permissions = arguments["permissions"] as? [Int],
permissions.count == types.count
let types = arguments["types"] as? [String],
let permissions = arguments["permissions"] as? [Int],
permissions.count == types.count
else {
throw PluginError(message: "Invalid Arguments!")
}

var typesToRead = Set<HKObjectType>()
var typesToWrite = Set<HKSampleType>()

for (index, key) in types.enumerated() {
if (key == HealthConstants.NUTRITION) {
if key == HealthConstants.NUTRITION {
for nutritionType in nutritionList {
guard let nutritionData = dataTypesDict[nutritionType] else {
print("Warning: Nutrition data type '\(nutritionType)' not found in dataTypesDict")
print(
"Warning: Nutrition data type '\(nutritionType)' not found in dataTypesDict"
)
continue
}
typesToWrite.insert(nutritionData)
let access = permissions[index]
switch access {
case 0:
typesToRead.insert(nutritionData)
case 1:
typesToWrite.insert(nutritionData)
default:
typesToRead.insert(nutritionData)
typesToWrite.insert(nutritionData)
}

}
} else {
let access = permissions[index]

if let dataType = dataTypesDict[key] {
switch access {
case 0:
Expand All @@ -139,90 +154,98 @@ class HealthDataOperations {
typesToWrite.insert(dataType)
}
}

if let characteristicsType = characteristicsTypesDict[key] {
switch access {
case 0:
typesToRead.insert(characteristicsType)
case 1:
throw PluginError(message: "Cannot request write permission for characteristic type \(characteristicsType)")
throw PluginError(
message:
"Cannot request write permission for characteristic type \(characteristicsType)"
)
default:
typesToRead.insert(characteristicsType)
}
}

if dataTypesDict[key] == nil && characteristicsTypesDict[key] == nil {
print("Warning: Health data type '\(key)' not found in dataTypesDict or characteristicsTypesDict")
print(
"Warning: Health data type '\(key)' not found in dataTypesDict or characteristicsTypesDict"
)
}

}
}

if #available(iOS 13.0, *) {
healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) {
(success, error) in
DispatchQueue.main.async {
result(success)
}

healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) {
(success, error) in
DispatchQueue.main.async {
result(success)
}
} else {
// TODO: Add proper error handling
result(false)
}

}

/// Delete health data by date range
/// - Parameters:
/// - call: Flutter method call
/// - result: Flutter result callback
func delete(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let arguments = call.arguments as? NSDictionary,
let dataTypeKey = arguments["dataTypeKey"] as? String else {
let dataTypeKey = arguments["dataTypeKey"] as? String
else {
print("Error: Missing dataTypeKey in arguments")
result(false)
return
}

// Check if it's a characteristic type - these cannot be deleted
if characteristicsTypesDict[dataTypeKey] != nil {
print("Info: Cannot delete characteristic type '\(dataTypeKey)' - these are read-only system values")
print(
"Info: Cannot delete characteristic type '\(dataTypeKey)' - these are read-only system values"
)
result(false)
return
}

let startTime = (arguments["startTime"] as? NSNumber) ?? 0
let endTime = (arguments["endTime"] as? NSNumber) ?? 0

let dateFrom = HealthUtilities.dateFromMilliseconds(startTime.doubleValue)
let dateTo = HealthUtilities.dateFromMilliseconds(endTime.doubleValue)

guard let dataType = dataTypesDict[dataTypeKey] else {
print("Warning: Health data type '\(dataTypeKey)' not found in dataTypesDict")
result(false)
return
}

let samplePredicate = HKQuery.predicateForSamples(
withStart: dateFrom, end: dateTo, options: .strictStartDate)
let ownerPredicate = HKQuery.predicateForObjects(from: HKSource.default())
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)

let deleteQuery = HKSampleQuery(
sampleType: dataType,
predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [samplePredicate, ownerPredicate]),
predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
samplePredicate, ownerPredicate,
]),
limit: HKObjectQueryNoLimit,
sortDescriptors: [sortDescriptor]
) { [weak self] x, samplesOrNil, error in
guard let self = self else { return }

guard let samplesOrNil = samplesOrNil, error == nil else {
print("Error querying \(dataType) samples: \(error?.localizedDescription ?? "Unknown error")")
print(
"Error querying \(dataType) samples: \(error?.localizedDescription ?? "Unknown error")"
)
DispatchQueue.main.async {
result(false)
}
return
}

// Chcek if there are any samples to delete
if samplesOrNil.isEmpty {
print("Info: No \(dataType) samples found in the specified date range.")
Expand All @@ -231,7 +254,7 @@ class HealthDataOperations {
}
return
}

// Delete the retrieved objects from the HealthKit store
self.healthStore.delete(samplesOrNil) { (success, error) in
if let err = error {
Expand All @@ -242,48 +265,49 @@ class HealthDataOperations {
}
}
}

healthStore.execute(deleteQuery)
}

/// Delete health data by UUID
/// - Parameters:
/// - call: Flutter method call
/// - result: Flutter result callback
func deleteByUUID(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
guard let arguments = call.arguments as? NSDictionary,
let uuidarg = arguments["uuid"] as? String,
let dataTypeKey = arguments["dataTypeKey"] as? String else {
let uuidarg = arguments["uuid"] as? String,
let dataTypeKey = arguments["dataTypeKey"] as? String
else {
throw PluginError(message: "Invalid Arguments - UUID or DataTypeKey invalid")
}

guard let dataTypeToRemove = dataTypesDict[dataTypeKey] else {
print("Warning: Health data type '\(dataTypeKey)' not found in dataTypesDict")
result(false)
return
}

guard let uuid = UUID(uuidString: uuidarg) else {
result(false)
return
}
let predicate = HKQuery.predicateForObjects(with: [uuid])

let query = HKSampleQuery(
sampleType: dataTypeToRemove,
predicate: predicate,
limit: 1,
sortDescriptors: nil
) { [weak self] query, samplesOrNil, error in
guard let self = self else { return }

guard let samples = samplesOrNil, !samples.isEmpty else {
DispatchQueue.main.async {
result(false)
}
return
}

self.healthStore.delete(samples) { success, error in
if let error = error {
print("Error deleting sample with UUID \(uuid): \(error.localizedDescription)")
Expand All @@ -293,7 +317,7 @@ class HealthDataOperations {
}
}
}

healthStore.execute(query)
}
}
2 changes: 1 addition & 1 deletion packages/health/ios/health.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
Pod::Spec.new do |s|
s.name = 'health'
s.version = '13.1.2'
s.version = '13.1.3'
s.summary = 'Wrapper for Apple\'s HealthKit on iOS and Google\'s Health Connect on Android.'
s.description = <<-DESC
Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android.
Expand Down
Loading