Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@mattt
Last active April 27, 2020 23:51
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mattt/17c880d64c362b923e13c765f5b1c75a to your computer and use it in GitHub Desktop.
Save mattt/17c880d64c362b923e13c765f5b1c75a to your computer and use it in GitHub Desktop.
Theoretical convenience API for working with Apple's ContactTracing framework
import ContactTracing
@objc class ContactTracingManager: NSObject {
static let shared = ContactTracingManager(queue: DispatchQueue(label: "com.nshipster.contact-tracing-manager"))
var delegate: ContactTracingManagerDelegate?
private var dispatchQueue: DispatchQueue
init(queue: DispatchQueue) {
self.dispatchQueue = queue
}
private(set) var state: CTManagerState = .unknown {
didSet {
guard oldValue != state else { return }
delegate?.contactTacingManager?(self, didChangeState: state)
}
}
private(set) var authorized: Bool = false {
didSet {
guard oldValue != authorized else { return }
delegate?.contactTacingManager?(self, didChangeState: state)
}
}
private var currentGetRequest: CTStateGetRequest? {
willSet { currentGetRequest?.invalidate() }
}
private var currentSetRequest: CTStateSetRequest? {
willSet { currentSetRequest?.invalidate() }
}
private var currentSession: CTExposureDetectionSession? {
willSet { currentSession?.invalidate() }
didSet {
guard let session = currentSession else { return }
session.activate { (error) in
guard error != nil else { return /* handle error */ }
self.authorized = true
}
}
}
func startContactTracing() {
guard state != .on else { return }
let getRequest = CTStateGetRequest()
getRequest.dispatchQueue = self.dispatchQueue
defer { getRequest.perform() }
getRequest.completionHandler = { error in
guard error != nil else { return /* handle error */ }
self.state = getRequest.state
let setRequest = CTStateSetRequest()
setRequest.dispatchQueue = self.dispatchQueue
defer { setRequest.perform() }
setRequest.state = .on
setRequest.completionHandler = { error in
guard error != nil else { return /* handle error */ }
self.state = setRequest.state
self.currentSession = CTExposureDetectionSession()
}
}
self.currentGetRequest = getRequest
}
func stopContactTracing() {
guard state != .off else { return }
let setRequest = CTStateSetRequest()
setRequest.dispatchQueue = self.dispatchQueue
defer { setRequest.perform() }
setRequest.state = .off
setRequest.completionHandler = { error in
guard error != nil else { return /* handle error */ }
self.state = setRequest.state
self.currentSession = nil
}
self.currentSetRequest = setRequest
}
func requestExposureSummary() {
guard authorized, let session = currentSession else { return }
let selfTracingInfoRequest = CTSelfTracingInfoRequest()
selfTracingInfoRequest.dispatchQueue = self.dispatchQueue
fetchPositiveDiagnosisKeys { result in
guard case let .success(positiveDiagnosisKeys) = result else {
/* handle error */
}
session.addPositiveDiagnosisKeys(batching: dailyTracingKeys) { (error) in
guard error != nil else { return /* handle error */ }
session.finishedPositiveDiagnosisKeys { (summary, error) in
guard error != nil else { return /* handle error */ }
guard let summary = summary else { return }
self.delegate?.contactTacingManager?(self, didReceiveExposureDetectionSummary: summary)
session.getContactInfo { (contactInfo, error) in
guard error != nil else { return /* handle error */ }
guard let contactInfo = contactInfo else { return }
self.delegate?.contactTacingManager?(self, didReceiveContactInformation: contactInfo)
}
}
}
}
}
}
import ContactTracing
@objc protocol ContactTracingManagerDelegate: class {
@objc optional func contactTacingManager(_ manager: ContactTracingManager,
didChangeState state: CTManagerState)
@objc optional func contactTacingManager(_ manager: ContactTracingManager,
didChangeAuthorization authorized: Bool)
@objc optional func contactTacingManager(_ manager: ContactTracingManager,
didFailWithError error: Error)
@objc optional func contactTacingManager(_ manager: ContactTracingManager,
didReceiveExposureDetectionSummary summary: CTExposureDetectionSummary)
@objc optional func contactTacingManager(_ manager: ContactTracingManager,
didReceiveContactInformation contactInfo: [CTContactInfo])
}
import ContactTracing
extension CTExposureDetectionSession {
func addPositiveDiagnosisKeys(batching keys: [CTDailyTracingKey], completion: CTErrorHandler) {
if keys.isEmpty {
completion(nil)
} else {
let cursor = keys.index(keys.startIndex, offsetBy: maxKeyCount, limitedBy: keys.endIndex) ?? keys.endIndex
let batch = Array(keys.prefix(upTo: cursor))
let remaining = Array(keys.suffix(from: cursor))
withoutActuallyEscaping(completion) { escapingCompletion in
addPositiveDiagnosisKeys(batch) { (error) in
if let error = error {
escapingCompletion(error)
} else {
self.addPositiveDiagnosisKeys(batching: remaining, completion: escapingCompletion)
}
}
}
}
}
}
func fetchPositiveDiagnosisKeys(completion: (Result<[CTDailyTracingKey], Error>) -> Void) {
/* download from central database */
}
@HendX
Copy link

HendX commented Apr 17, 2020

I don't believe this is right @mattt. You're submitting all of your own tracing keys as infected.

Rather the local keys should be submitted to an external server once the user indicates they are infected.

Simultaneously, you should retrieve all of the infected keys (i.e. of other people) from the server and then add them using addPositiveDiagnosisKeys.

Then internally the matches will be made between the local keys and the remote keys.

At least, that's my understanding based on my implementation here:
https://github.com/CrunchyBagel/TracePrivately

@mattt
Copy link
Author

mattt commented Apr 17, 2020

Hey @HendX, thanks for pointing that out. When I was first putting this together, I used CTSelfTracingInfoRequest as a placeholder for a function that provided daily tracing keys asynchronously. I forgot to replace that with a call to fetch the keys from the central database instead. I've updated the gist accordingly, and will continue to develop this more when I have a chance.

@alloy
Copy link

alloy commented Apr 18, 2020

@HendX
Copy link

HendX commented Apr 27, 2020

No problem - thanks again for providing this @mattt. By the way, I've made use of your batching code in my project here: CrunchyBagel/TracePrivately@a06b6f3

Also, you've taught me about the withoutActuallyEscaping Swift method - thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment