Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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 */
}
@mattt
Copy link
Author

mattt commented Apr 12, 2020

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to https://unlicense.org

@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