-
-
Save mattt/17c880d64c362b923e13c765f5b1c75a to your computer and use it in GitHub Desktop.
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 */ | |
} |
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
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.
@mattt Made minor spelling and build fixes: https://github.com/alloy/NSHipster-ContactTracingManager
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!
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