Stabilize iOS/watchOS/tvOS apps and add cross-platform audit remediation
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import HealthKit
|
||||
import SharedCore
|
||||
|
||||
struct HealthKitWorkoutData {
|
||||
var caloriesBurned: Double?
|
||||
@@ -16,111 +17,155 @@ struct HealthKitWorkoutData {
|
||||
}
|
||||
|
||||
class HealthKitHelper {
|
||||
// this is dirty and i dont care
|
||||
var returnCount = 0
|
||||
private let runtimeReporter = RuntimeReporter.shared
|
||||
let healthStore = HKHealthStore()
|
||||
|
||||
var healthKitWorkoutData = HealthKitWorkoutData(
|
||||
caloriesBurned: nil,
|
||||
minHeartRate: nil,
|
||||
maxHeartRate: nil,
|
||||
avgHeartRate: nil)
|
||||
|
||||
var completion: ((HealthKitWorkoutData?) -> Void)?
|
||||
|
||||
|
||||
func getDetails(forHealthKitUUID uuid: UUID, completion: @escaping ((HealthKitWorkoutData?) -> Void)) {
|
||||
self.completion = completion
|
||||
self.returnCount = 0
|
||||
|
||||
print("get details \(uuid.uuidString)")
|
||||
|
||||
runtimeReporter.recordInfo("Fetching HealthKit workout details", metadata: ["uuid": uuid.uuidString])
|
||||
|
||||
let query = HKSampleQuery(sampleType: HKWorkoutType.workoutType(),
|
||||
predicate: HKQuery.predicateForObject(with: uuid),
|
||||
limit: HKObjectQueryNoLimit,
|
||||
limit: 1,
|
||||
sortDescriptors: nil)
|
||||
{ (sampleQuery, results, error ) -> Void in
|
||||
if let queryError = error {
|
||||
self.shitReturned()
|
||||
self.shitReturned()
|
||||
print( "There was an error while reading the samples: \(queryError.localizedDescription)")
|
||||
self.completion?(nil)
|
||||
} else {
|
||||
for samples: HKSample in results! {
|
||||
let workout: HKWorkout = (samples as! HKWorkout)
|
||||
self.getTotalBurned(forWorkout: workout)
|
||||
self.getHeartRateStuff(forWorkout: workout)
|
||||
print("got workout")
|
||||
{ [weak self] (_, results, error) -> Void in
|
||||
guard let self else {
|
||||
DispatchQueue.main.async {
|
||||
completion(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let queryError = error {
|
||||
self.runtimeReporter.recordError(
|
||||
"Failed querying HealthKit workout",
|
||||
metadata: ["error": queryError.localizedDescription]
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
completion(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let workout = results?.compactMap({ $0 as? HKWorkout }).first else {
|
||||
self.runtimeReporter.recordWarning("No HealthKit workout found for UUID", metadata: ["uuid": uuid.uuidString])
|
||||
DispatchQueue.main.async {
|
||||
completion(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.collectDetails(forWorkout: workout, completion: completion)
|
||||
}
|
||||
|
||||
|
||||
healthStore.execute(query)
|
||||
}
|
||||
|
||||
func getHeartRateStuff(forWorkout workout: HKWorkout) {
|
||||
print("get heart")
|
||||
let heartType = HKQuantityType.quantityType(forIdentifier: .heartRate)
|
||||
let heartPredicate: NSPredicate? = HKQuery.predicateForSamples(withStart: workout.startDate,
|
||||
end: workout.endDate,
|
||||
options: HKQueryOptions.strictEndDate)
|
||||
|
||||
let heartQuery = HKStatisticsQuery(quantityType: heartType!,
|
||||
|
||||
private func collectDetails(forWorkout workout: HKWorkout, completion: @escaping ((HealthKitWorkoutData?) -> Void)) {
|
||||
let aggregateQueue = DispatchQueue(label: "com.werkout.healthkit.aggregate")
|
||||
var workoutData = HealthKitWorkoutData(
|
||||
caloriesBurned: nil,
|
||||
minHeartRate: nil,
|
||||
maxHeartRate: nil,
|
||||
avgHeartRate: nil
|
||||
)
|
||||
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
getTotalBurned(forWorkout: workout) { calories in
|
||||
aggregateQueue.async {
|
||||
workoutData.caloriesBurned = calories
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
getHeartRateStuff(forWorkout: workout) { heartRateData in
|
||||
aggregateQueue.async {
|
||||
workoutData.minHeartRate = heartRateData?.minHeartRate
|
||||
workoutData.maxHeartRate = heartRateData?.maxHeartRate
|
||||
workoutData.avgHeartRate = heartRateData?.avgHeartRate
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
let hasValues = workoutData.caloriesBurned != nil ||
|
||||
workoutData.minHeartRate != nil ||
|
||||
workoutData.maxHeartRate != nil ||
|
||||
workoutData.avgHeartRate != nil
|
||||
|
||||
completion(hasValues ? workoutData : nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func getHeartRateStuff(forWorkout workout: HKWorkout, completion: @escaping ((HealthKitWorkoutData?) -> Void)) {
|
||||
guard let heartType = HKQuantityType.quantityType(forIdentifier: .heartRate) else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let heartPredicate = HKQuery.predicateForSamples(withStart: workout.startDate,
|
||||
end: workout.endDate,
|
||||
options: HKQueryOptions.strictEndDate)
|
||||
|
||||
let heartQuery = HKStatisticsQuery(quantityType: heartType,
|
||||
quantitySamplePredicate: heartPredicate,
|
||||
options: [.discreteAverage, .discreteMin, .discreteMax],
|
||||
completionHandler: {(query: HKStatisticsQuery, result: HKStatistics?, error: Error?) -> Void in
|
||||
if let result = result,
|
||||
let minValue = result.minimumQuantity(),
|
||||
let maxValue = result.maximumQuantity(),
|
||||
let avgValue = result.averageQuantity() {
|
||||
|
||||
let _minHeartRate = minValue.doubleValue(
|
||||
for: HKUnit(from: "count/min")
|
||||
completionHandler: { [weak self] (_, result, error) -> Void in
|
||||
if let error {
|
||||
self?.runtimeReporter.recordError(
|
||||
"Failed querying HealthKit heart rate stats",
|
||||
metadata: ["error": error.localizedDescription]
|
||||
)
|
||||
|
||||
let _maxHeartRate = maxValue.doubleValue(
|
||||
for: HKUnit(from: "count/min")
|
||||
)
|
||||
|
||||
let _avgHeartRate = avgValue.doubleValue(
|
||||
for: HKUnit(from: "count/min")
|
||||
)
|
||||
self.healthKitWorkoutData.avgHeartRate = _avgHeartRate
|
||||
self.healthKitWorkoutData.minHeartRate = _minHeartRate
|
||||
self.healthKitWorkoutData.maxHeartRate = _maxHeartRate
|
||||
print("got heart")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
self.shitReturned()
|
||||
|
||||
guard let result,
|
||||
let minValue = result.minimumQuantity(),
|
||||
let maxValue = result.maximumQuantity(),
|
||||
let avgValue = result.averageQuantity() else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let unit = HKUnit(from: "count/min")
|
||||
let data = HealthKitWorkoutData(
|
||||
caloriesBurned: nil,
|
||||
minHeartRate: minValue.doubleValue(for: unit),
|
||||
maxHeartRate: maxValue.doubleValue(for: unit),
|
||||
avgHeartRate: avgValue.doubleValue(for: unit)
|
||||
)
|
||||
completion(data)
|
||||
})
|
||||
healthStore.execute(heartQuery)
|
||||
}
|
||||
|
||||
func getTotalBurned(forWorkout workout: HKWorkout) {
|
||||
print("get total burned")
|
||||
let calType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)
|
||||
let calPredicate: NSPredicate? = HKQuery.predicateForSamples(withStart: workout.startDate,
|
||||
end: workout.endDate,
|
||||
options: HKQueryOptions.strictEndDate)
|
||||
|
||||
let calQuery = HKStatisticsQuery(quantityType: calType!,
|
||||
|
||||
private func getTotalBurned(forWorkout workout: HKWorkout, completion: @escaping ((Double?) -> Void)) {
|
||||
guard let calType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned) else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let calPredicate = HKQuery.predicateForSamples(withStart: workout.startDate,
|
||||
end: workout.endDate,
|
||||
options: HKQueryOptions.strictEndDate)
|
||||
|
||||
let calQuery = HKStatisticsQuery(quantityType: calType,
|
||||
quantitySamplePredicate: calPredicate,
|
||||
options: [.cumulativeSum],
|
||||
completionHandler: {(query: HKStatisticsQuery, result: HKStatistics?, error: Error?) -> Void in
|
||||
if let result = result {
|
||||
self.healthKitWorkoutData.caloriesBurned = result.sumQuantity()?.doubleValue(for: HKUnit.kilocalorie()) ?? -1
|
||||
print("got total burned")
|
||||
completionHandler: { [weak self] (_, result, error) -> Void in
|
||||
if let error {
|
||||
self?.runtimeReporter.recordError(
|
||||
"Failed querying HealthKit calories",
|
||||
metadata: ["error": error.localizedDescription]
|
||||
)
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
self.shitReturned()
|
||||
|
||||
completion(result?.sumQuantity()?.doubleValue(for: HKUnit.kilocalorie()))
|
||||
})
|
||||
healthStore.execute(calQuery)
|
||||
}
|
||||
|
||||
func shitReturned() {
|
||||
DispatchQueue.main.async {
|
||||
self.returnCount += 1
|
||||
print("\(self.returnCount)")
|
||||
if self.returnCount == 2 {
|
||||
self.completion?(self.healthKitWorkoutData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user