Skip to content

Commit

Permalink
Merge pull request #4 from Open-Health-Manager/healthkit-uspstfapi
Browse files Browse the repository at this point in the history
Add HealthKit and USPSTF API support
  • Loading branch information
dmpotter44 authored Jun 3, 2022
2 parents 8d20d78 + 3a6a83b commit 611d17d
Show file tree
Hide file tree
Showing 25 changed files with 2,576 additions and 109 deletions.
5 changes: 4 additions & 1 deletion assets/config/config.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"fhirBase": "http://localhost:8080/fhir/"
"fhirBase": "http://localhost:8080/fhir/",
"uspstfApi" : {
"key": null
}
}
12 changes: 10 additions & 2 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 51;
objects = {

/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
2E39B16C2821EAF400D46B16 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E39B16B2821EAF400D46B16 /* HealthKit.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7C57E62ABBCB1A66615308E9 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4ED6591588B9F86D5496C8A7 /* Pods_Runner.framework */; };
Expand All @@ -32,6 +33,8 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
2E39B16A2821EAF400D46B16 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
2E39B16B2821EAF400D46B16 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; };
33FCE8195B8564D12DB1C2C9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
48214973EDA7F9CC36BED4A8 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
Expand All @@ -55,6 +58,7 @@
buildActionMask = 2147483647;
files = (
7C57E62ABBCB1A66615308E9 /* Pods_Runner.framework in Frameworks */,
2E39B16C2821EAF400D46B16 /* HealthKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -68,7 +72,6 @@
48214973EDA7F9CC36BED4A8 /* Pods-Runner.release.xcconfig */,
B97BFD0F5FCF50871A17B81E /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
Expand Down Expand Up @@ -105,6 +108,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
2E39B16A2821EAF400D46B16 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
Expand All @@ -120,6 +124,7 @@
9E0DEFE3C0AA18067902E79F /* Frameworks */ = {
isa = PBXGroup;
children = (
2E39B16B2821EAF400D46B16 /* HealthKit.framework */,
4ED6591588B9F86D5496C8A7 /* Pods_Runner.framework */,
);
name = Frameworks;
Expand Down Expand Up @@ -355,6 +360,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
Expand Down Expand Up @@ -483,6 +489,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
Expand All @@ -505,6 +512,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
Expand Down
158 changes: 151 additions & 7 deletions ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,157 @@
import UIKit
import Flutter
import HealthKit

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
@available(iOS 12.0, *)
static let supportedTypes = [
// // For now, only request access to vital sign records
// HKClinicalTypeIdentifier.allergyRecord,
// HKClinicalTypeIdentifier.conditionRecord,
// HKClinicalTypeIdentifier.immunizationRecord,
// HKClinicalTypeIdentifier.labResultRecord,
// HKClinicalTypeIdentifier.medicationRecord,
// HKClinicalTypeIdentifier.procedureRecord,
HKClinicalTypeIdentifier.vitalSignRecord
];
lazy var healthStore = HKHealthStore()

override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let healthKitChannel = FlutterMethodChannel(name: "mitre.org/rosie/healthkit", binaryMessenger: controller.binaryMessenger)
healthKitChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch (call.method) {
case "isHealthDataAvailable":
result(HKHealthStore.isHealthDataAvailable())
case "requestAccess":
self?.requestHealthKitAccess(result: result)
case "supportedClinicalTypes":
self?.supportedClinicalTypes(result: result)
case "queryClinicalRecords":
self?.queryClinicalRecords(call: call, result: result)
default:
result(FlutterMethodNotImplemented)
}
})

GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

func requestHealthKitAccess(result: @escaping FlutterResult) {
if #available(iOS 12.0, *) {
// Create the sample types if possible
var types = Set<HKObjectType>()
for type in AppDelegate.supportedTypes {
if let clinicalType = HKObjectType.clinicalType(forIdentifier: type) {
types.insert(clinicalType)
}
}
healthStore.requestAuthorization(toShare: nil, read: types, completion: { success, error in
// The result happens in a background thread, but we want to invoke Flutter only from the main thread, so:
DispatchQueue.main.async {
if let error = error {
result(FlutterError(code: "HealthKitError", message: error.localizedDescription, details: error))
} else {
result(success)
}
}
})
} else {
result(healthKitNotSupported())
}
}

func supportedClinicalTypes(result: FlutterResult) {
// For this, just create a list of strings
if #available(iOS 12.0, *) {
print("Building supported clinical types")
result(AppDelegate.supportedTypes.map { $0.rawValue })
} else {
result([])
}
}

func queryClinicalRecords(call: FlutterMethodCall, result: @escaping FlutterResult) {
if #available(iOS 12.0, *) {
// Create the query. For this method we expect an argument that's a string
guard let typeString = call.arguments as? String else {
result(FlutterError(code: "MissingArgumentsError", message: "Missing required argument type", details: nil))
return
}
// This may be invalid but we won't know until...
let typeIdentifier = HKClinicalTypeIdentifier(rawValue: typeString)
// ...we try and create an HKObjectType from it
guard let type = HKObjectType.clinicalType(forIdentifier: typeIdentifier) else {
result(FlutterError(code: "HealthKitError", message: "Unsupported type", details: typeString))
return
}
let query = HKSampleQuery(sampleType: type, predicate: nil, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { query, samples, error in
guard let actualSamples = samples else {
result(FlutterError(code: "HealthKitError", message: error?.localizedDescription ?? "No error given", details: error))
return
}
// And now that we have the query, export them as JSON strings (they're not decoded here, the Dart side can do that)
var records: [[String: String?]] = []
for sample in actualSamples {
let jsonData = createResponse(fromClinicalRecord: sample)
if let json = jsonData {
records.append(json)
}
}
result(records)
}
healthStore.execute(query)
} else {
result(healthKitNotSupported())
}
}
}

// MARK: Utility functions

func healthKitNotSupported() -> FlutterError {
return FlutterError(code: "HealthKitUnavailable", message: "HealthKit not available", details: nil)
}

@available(iOS 12.0, *)
func createResponse(fromClinicalRecord sample: HKSample) -> [String: String?]? {
guard let record = sample as? HKClinicalRecord else { return nil }
guard let fhirResource = record.fhirResource else { return nil }
guard let jsonData = String(data: fhirResource.data, encoding: .utf8) else { return nil }
return [
"fhirVersion": escapeFhirVersion(fromFhirResource: fhirResource),
"sourceUrl": fhirResource.sourceURL?.absoluteString,
"resource": jsonData
];
}

@available(iOS 12.0, *)
func escapeFhirVersion(fromFhirResource resource: HKFHIRResource) -> String {
if #available(iOS 14.0, *) {
// Rather than attempt to encode the entire thing, use the "release"
switch (resource.fhirVersion.fhirRelease) {
case .dstu2:
return "dstu2"
case .r4:
return "r4"
default:
return "unknown"
}
} else {
return "dstu2";
}
}

@available(iOS 12.0, *)
func extractJSON(fromClinicalRecord record: HKClinicalRecord) -> String? {
guard let fhirResource = record.fhirResource else { return nil }
// This call is an optional constructor: if it fails, it returns nil.
// Fortunately if it fails, it should return nil.
return String(data: fhirResource.data, encoding: .utf8)
}
6 changes: 6 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSHealthClinicalHealthRecordsShareUsageDescription</key>
<string>Your clinical health records are used to help determine your overall health and determine actions that may improve your health.</string>
<key>NSHealthShareUsageDescription</key>
<string>Your health data are used to help determine your overall health and determine actions that may improve your health.</string>
<key>NSHealthUpdateUsageDescription</key>
<string>This permission should not be requested.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
Expand Down
12 changes: 12 additions & 0 deletions ios/Runner/Runner.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.healthkit</key>
<true/>
<key>com.apple.developer.healthkit.access</key>
<array>
<string>health-records</string>
</array>
</dict>
</plist>
14 changes: 14 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2022 The MITRE Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import 'package:flutter/material.dart';
import 'src/app.dart';

Expand Down
Loading

0 comments on commit 611d17d

Please sign in to comment.