diff --git a/KeyServer/KeyServer.yaml b/KeyServer/KeyServer.yaml index 9293367..2388202 100644 --- a/KeyServer/KeyServer.yaml +++ b/KeyServer/KeyServer.yaml @@ -139,6 +139,9 @@ paths: type: integer example: 1234 description: The rolling start number for the key. + l: + type: integer + description: Represents the calculated risk level from 1-100 deleted_keys: description: | A list of keys that have subsequently been marked as not infected. Client should remove them from their cache. @@ -214,6 +217,9 @@ paths: type: integer example: 1234 description: The rolling start number for the key. + l: + type: integer + description: Represents the calculated risk level from 1-100 form: type: array description: Additional form data gathered from user. Each item diff --git a/KeyServer/sample/htdocs/infected.php b/KeyServer/sample/htdocs/infected.php index 4b5f013..530c018 100644 --- a/KeyServer/sample/htdocs/infected.php +++ b/KeyServer/sample/htdocs/infected.php @@ -33,7 +33,7 @@ } } -$stmt = $db->prepare('SELECT infected_key, rolling_start_number FROM infected_keys WHERE status = :s AND status_updated >= :t'); +$stmt = $db->prepare('SELECT infected_key, rolling_start_number, risk_level FROM infected_keys WHERE status = :s AND status_updated >= :t'); $stmt->bindValue(':t', $time, SQLITE3_INTEGER); $stmt->bindValue(':s', 'A', SQLITE3_TEXT); @@ -48,7 +48,8 @@ while (($row = $result->fetchArray(SQLITE3_NUM))) { $keys[] = array( 'd' => base64_decode($row[0]), - 'r' => (int) $row[1] + 'r' => (int) $row[1], + 'l' => (int) $row[2] ); } } @@ -56,7 +57,8 @@ while (($row = $result->fetchArray(SQLITE3_NUM))) { $keys[] = array( 'd' => $row[0], - 'r' => (int) $row[1] + 'r' => (int) $row[1], + 'l' => (int) $row[2] ); } } diff --git a/KeyServer/sample/htdocs/submit.php b/KeyServer/sample/htdocs/submit.php index dadbfbc..036032f 100644 --- a/KeyServer/sample/htdocs/submit.php +++ b/KeyServer/sample/htdocs/submit.php @@ -1,6 +1,8 @@ lastInsertRowID(); - $stmt = $db->prepare('INSERT INTO infected_keys (infected_key, rolling_start_number, timestamp, status, status_updated, submission_id) VALUES (:k, :r, :t, :s, :d, :i)'); + $stmt = $db->prepare('INSERT INTO infected_keys (infected_key, rolling_start_number, risk_level, timestamp, status, status_updated, submission_id) VALUES (:k, :r, :l, :t, :s, :d, :i)'); // It's possible there are no keys with a submission, and a placeholder record is created so subsequent keys can be recorded foreach ($json['keys'] as $key) { $encodedKey = $key['d']; $rollingStartNumber = $key['r']; + $riskLevel = $key['l']; $stmt->bindValue(':k', $encodedKey, SQLITE3_TEXT); $stmt->bindValue(':r', $rollingStartNumber, SQLITE3_INTEGER); + $stmt->bindValue(':l', $riskLevel, SQLITE3_INTEGER); $stmt->bindValue(':t', $time, SQLITE3_INTEGER); $stmt->bindValue(':s', 'P', SQLITE3_TEXT); // Pending state, must be approved $stmt->bindValue(':d', $time, SQLITE3_INTEGER); diff --git a/KeyServer/sample/schema.sql b/KeyServer/sample/schema.sql index 8d7f29c..194c8a8 100644 --- a/KeyServer/sample/schema.sql +++ b/KeyServer/sample/schema.sql @@ -29,6 +29,7 @@ CREATE INDEX infected_key_submissions_timestamp ON infected_key_submissions (tim CREATE TABLE infected_keys ( infected_key STRING, rolling_start_number INTEGER, + risk_level INTEGER, status TEXT, timestamp INTEGER, status_updated INTEGER, diff --git a/TracePrivately.xcodeproj/project.pbxproj b/TracePrivately.xcodeproj/project.pbxproj index 83229b1..7f118b6 100644 --- a/TracePrivately.xcodeproj/project.pbxproj +++ b/TracePrivately.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 6614BB052452B9D900885F23 /* ExposureDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6614BB042452B9D900885F23 /* ExposureDetailsViewController.swift */; }; 6621056D245251B10011EB42 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6621056C245251B10011EB42 /* main.swift */; }; 6621057124525FC90011EB42 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6621057324525FC90011EB42 /* Localizable.strings */; }; + 663BD8B9245A510C00A3E029 /* ExposureDataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663BD8B8245A510C00A3E029 /* ExposureDataTypes.swift */; }; 66437F76244A86AE000B8C6C /* Disease.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66437F75244A86AE000B8C6C /* Disease.swift */; }; 66437F9E244A9A02000B8C6C /* TracePrivately.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 66437F9C244A9A02000B8C6C /* TracePrivately.xcdatamodeld */; }; 66437FA0244A9B2C000B8C6C /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66437F9F244A9B2C000B8C6C /* DataManager.swift */; }; @@ -93,6 +94,9 @@ 662105782452602D0011EB42 /* hi-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hi-IN"; path = "hi-IN.lproj/Localizable.strings"; sourceTree = ""; }; 6625219B2453955F00C68AD1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 6625219C2453957100C68AD1 /* sr-RS */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sr-RS"; path = "sr-RS.lproj/Localizable.strings"; sourceTree = ""; }; + 663BD8B7245A3F8100A3E029 /* TracePrivately.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TracePrivately.entitlements; sourceTree = ""; }; + 663BD8B8245A510C00A3E029 /* ExposureDataTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExposureDataTypes.swift; sourceTree = ""; }; + 663BD8BA245A537900A3E029 /* Model 8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 8.xcdatamodel"; sourceTree = ""; }; 66437F75244A86AE000B8C6C /* Disease.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Disease.swift; sourceTree = ""; }; 66437F9D244A9A02000B8C6C /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; 66437F9F244A9B2C000B8C6C /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = ""; }; @@ -188,6 +192,7 @@ 66CB7BD02453D51A0044D32B /* ENMockFramework.swift */, 66437F75244A86AE000B8C6C /* Disease.swift */, 66437F9F244A9B2C000B8C6C /* DataManager.swift */, + 663BD8B8245A510C00A3E029 /* ExposureDataTypes.swift */, 668CE842244D01F50065960A /* ContactTraceManager.swift */, 66C4BBC424564B56008AACC2 /* ExposureNotificationConfig.swift */, 66CB95BC244FB51000D775B6 /* ActionButton.swift */, @@ -244,6 +249,7 @@ 66F818692448FD5C0043AC2D /* TracePrivately */ = { isa = PBXGroup; children = ( + 663BD8B7245A3F8100A3E029 /* TracePrivately.entitlements */, 39BD8847244C6CF000EB8B6C /* Storyboards */, 66437F77244A8A4C000B8C6C /* Classes */, 66F818732448FD5E0043AC2D /* Assets.xcassets */, @@ -386,6 +392,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 663BD8B9245A510C00A3E029 /* ExposureDataTypes.swift in Sources */, 66793892245460B600868B84 /* KeyServer.swift in Sources */, 66848C2D244D5F8800497D2A /* AsyncBlockOperation.swift in Sources */, 66EAEB62244DA72E0044FF70 /* SubmitInfectionViewController.swift in Sources */, @@ -630,6 +637,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = TracePrivately/TracePrivately.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = CHZS3BHM57; INFOPLIST_FILE = TracePrivately/Info.plist; @@ -651,6 +659,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = TracePrivately/TracePrivately.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = CHZS3BHM57; INFOPLIST_FILE = TracePrivately/Info.plist; @@ -702,6 +711,7 @@ 66437F9C244A9A02000B8C6C /* TracePrivately.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 663BD8BA245A537900A3E029 /* Model 8.xcdatamodel */, 664C5236245802C9003EBE63 /* Model 7.xcdatamodel */, 66BF646624578E2B00492070 /* Model 6.xcdatamodel */, 66CB7BD22453EB190044D32B /* Model 5.xcdatamodel */, @@ -710,7 +720,7 @@ 668D4B22244D705100D90C26 /* Model 2.xcdatamodel */, 66437F9D244A9A02000B8C6C /* Model.xcdatamodel */, ); - currentVersion = 664C5236245802C9003EBE63 /* Model 7.xcdatamodel */; + currentVersion = 663BD8BA245A537900A3E029 /* Model 8.xcdatamodel */; path = TracePrivately.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/TracePrivately/Classes/ContactTraceManager.swift b/TracePrivately/Classes/ContactTraceManager.swift index bcd499b..26a6b38 100644 --- a/TracePrivately/Classes/ContactTraceManager.swift +++ b/TracePrivately/Classes/ContactTraceManager.swift @@ -6,6 +6,9 @@ import Foundation import UserNotifications import UIKit +#if canImport(ExposureNotification) +import ExposureNotification +#endif class ContactTraceManager: NSObject { @@ -21,7 +24,8 @@ class ContactTraceManager: NSObject { static let backgroundProcessingTaskIdentifier = "ctm.processor" - fileprivate var exposureDetectionSession: ENExposureDetectionSession? + fileprivate var enManager: ENManager? + fileprivate var enDetectionSession: ENExposureDetectionSession? private var _isUpdatingEnabledState = false @objc dynamic var isUpdatingEnabledState: Bool { @@ -123,7 +127,7 @@ extension ContactTraceManager { self.saveNewInfectedKeys(keys: response.keys) { numNewKeys, error in self.saveLastReceivedInfectedKeys(date: response.date) - guard let session = self.exposureDetectionSession else { + guard let session = self.enDetectionSession else { self.isUpdatingExposures = false completion(nil) return @@ -137,10 +141,12 @@ extension ContactTraceManager { } } - fileprivate func addAndFinalizeKeys(session: ENExposureDetectionSession, keys: [ENTemporaryExposureKey], completion: @escaping (Swift.Error?) -> Void) { + fileprivate func addAndFinalizeKeys(session: ENExposureDetectionSession, keys: [TPTemporaryExposureKey], completion: @escaping (Swift.Error?) -> Void) { - session.batchAddDiagnosisKeys(inKeys: keys) { error in - session.finishedDiagnosisKeysWithCompletion { summary, error in + let k: [ENTemporaryExposureKey] = keys.map { $0.enExposureKey } + + session.batchAddDiagnosisKeys(k) { error in + session.finishedDiagnosisKeys { summary, error in guard let summary = summary else { completion(error) return @@ -155,9 +161,9 @@ extension ContactTraceManager { } // Documentation says use a reasonable number, such as 100 - let maxCount: UInt32 = 100 + let maximumCount: Int = 100 - self.getExposures(session: session, maxCount: maxCount, exposures: []) { exposures, error in + self.getExposures(session: session, maximumCount: maximumCount, exposures: []) { exposures, error in guard let exposures = exposures else { completion(error) return @@ -179,26 +185,28 @@ extension ContactTraceManager { } // Recursively retrieves exposures until all are received - private func getExposures(session: ENExposureDetectionSession, maxCount: UInt32, exposures: [ENExposureInfo], completion: @escaping ([ENExposureInfo]?, Swift.Error?) -> Void) { - session.getExposureInfoWithMaxCount(maxCount: maxCount) { newExposures, inDone, error in + private func getExposures(session: ENExposureDetectionSession, maximumCount: Int, exposures: [TPExposureInfo], completion: @escaping ([TPExposureInfo]?, Swift.Error?) -> Void) { + + session.getExposureInfo(withMaximumCount: maximumCount) { newExposures, inDone, error in - if let error = error { + guard let newExposures = newExposures else { completion(exposures, error) return } - let allExposures = exposures + (newExposures ?? []) + let allExposures = exposures + newExposures.map { $0.tpExposureInfo } if inDone { completion(allExposures, nil) } else { - self.getExposures(session: session, maxCount: maxCount, exposures: allExposures, completion: completion) + self.getExposures(session: session, maximumCount: maximumCount, exposures: allExposures, completion: completion) } } } - private func saveNewInfectedKeys(keys: [ENTemporaryExposureKey], completion: @escaping (_ numNewRemoteKeys: Int, Swift.Error?) -> Void) { + private func saveNewInfectedKeys(keys: [TPTemporaryExposureKey], completion: @escaping (_ numNewRemoteKeys: Int, Swift.Error?) -> Void) { + DataManager.shared.saveInfectedKeys(keys: keys) { numNewKeys, error in if let error = error { completion(0, error) @@ -323,43 +331,59 @@ extension ContactTraceManager { self.isUpdatingEnabledState = true - let settings = ENSettings(enableState: true) + self.enManager?.invalidate() - let request = ENSettingsChangeRequest(settings: settings) - request.activateWithCompletion { error in - defer { - request.invalidate() - } - + let manager = ENManager() + + switch manager.exposureNotificationStatus { + case .active: print("ACTIVE") + case .bluetoothOff: print("BLUETOOTH OFF") + case .disabled: print("DISABLED") + case .restricted: print("RESTRICTED") + case .unknown: print("UNKNOWN") + } + + self.enManager = manager + + manager.activate { error in + if let error = error { + manager.invalidate() + self.isUpdatingEnabledState = false self.isContactTracingEnabled = false completion(error) return } - - self.startExposureChecking { error in - - if error != nil { - let settings = ENSettings(enableState: false) - let request = ENSettingsChangeRequest(settings: settings) - request.activateWithCompletion { _ in - defer { - request.invalidate() - } + + manager.setExposureNotificationEnabled(true) { error in + if let error = error { + manager.invalidate() + + print("ERROR: \(error)") + + self.isUpdatingEnabledState = false + self.isContactTracingEnabled = false + completion(error) + return + } + + self.startExposureChecking { error in + + if error != nil { + manager.invalidate() self.isUpdatingEnabledState = false self.isContactTracingEnabled = false completion(error) + return } - - return + + self.isContactTracingEnabled = true + self.isUpdatingEnabledState = false + + completion(error) } - - self.isContactTracingEnabled = true - self.isUpdatingEnabledState = false - - completion(error) } } } @@ -372,19 +396,14 @@ extension ContactTraceManager { self.isUpdatingEnabledState = true self.stopExposureChecking() - self.exposureDetectionSession?.invalidate() - self.exposureDetectionSession = nil - - let settings = ENSettings(enableState: false) - let request = ENSettingsChangeRequest(settings: settings) - request.activateWithCompletion { _ in - defer { - request.invalidate() - } + self.enDetectionSession?.invalidate() + self.enDetectionSession = nil - self.isContactTracingEnabled = false - self.isUpdatingEnabledState = false - } + self.enManager?.invalidate() + self.enManager = nil + + self.isContactTracingEnabled = false + self.isUpdatingEnabledState = false } } @@ -393,14 +412,21 @@ extension ContactTraceManager { let dispatchGroup = DispatchGroup() let session = ENExposureDetectionSession() + + let configuration = ENExposureConfiguration() + + // TODO: Handle the configuration correctly + /* session.attenuationThreshold = self.config.session.attenuationThreshold session.durationThreshold = self.config.session.durationThreshold + */ + session.configuration = configuration var sessionError: Swift.Error? dispatchGroup.enter() - session.activateWithCompletion { error in + session.activate { error in if let error = error { sessionError = error @@ -435,7 +461,7 @@ extension ContactTraceManager { } } - self.exposureDetectionSession = session + self.enDetectionSession = session dispatchGroup.notify(queue: .main) { let error = sessionError @@ -444,7 +470,7 @@ extension ContactTraceManager { } fileprivate func stopExposureChecking() { - self.exposureDetectionSession = nil + self.enDetectionSession = nil } } @@ -459,32 +485,67 @@ extension ContactTraceManager: UNUserNotificationCenterDelegate { extension ENExposureDetectionSession { // Modified from https://gist.github.com/mattt/17c880d64c362b923e13c765f5b1c75a - func batchAddDiagnosisKeys(inKeys keys: [ENTemporaryExposureKey], completion: @escaping ENErrorHandler) { + func batchAddDiagnosisKeys(_ keys: [ENTemporaryExposureKey], completion: @escaping ENErrorHandler) { guard !keys.isEmpty else { completion(nil) return } - guard maxKeyCount > 0 else { + guard maximumKeyCount > 0 else { completion(nil) return } - let cursor = keys.index(keys.startIndex, offsetBy: maxKeyCount, limitedBy: keys.endIndex) ?? keys.endIndex + let cursor = keys.index(keys.startIndex, offsetBy: maximumKeyCount, limitedBy: keys.endIndex) ?? keys.endIndex let batch = Array(keys.prefix(upTo: cursor)) let remaining = Array(keys.suffix(from: cursor)) print("Adding: \(batch)") // withoutActuallyEscaping(completion) { escapingCompletion in - addDiagnosisKeys(inKeys: batch) { error in + addDiagnosisKeys(batch) { error in if let error = error { completion(error) } else { - self.batchAddDiagnosisKeys(inKeys: remaining, completion: completion) + self.batchAddDiagnosisKeys(remaining, completion: completion) } } // } } } + +extension ContactTraceManager { + func retrieveSelfDiagnosisKeys(completion: @escaping ([TPTemporaryExposureKey]?, Swift.Error?) -> Void) { + + guard let manager = self.enManager else { + // XXX: Shouldn't get here, but handle this error better + completion(nil, nil) + return + } + + manager.getDiagnosisKeys { keys, error in + guard let keys = keys else { + completion(nil, error) + return + } + + let k: [TPTemporaryExposureKey] = keys.map { $0.tpExposureKey } + + completion(k, nil) + } + } +} + +extension ContactTraceManager { + func resetAllData(completion: @escaping (Swift.Error?) -> Void) { + guard let manager = self.enManager else { + completion(nil) + return + } + + manager.resetAllData { error in + completion(error) + } + } +} diff --git a/TracePrivately/Classes/DataManager.swift b/TracePrivately/Classes/DataManager.swift index 173f311..9d98ce3 100644 --- a/TracePrivately/Classes/DataManager.swift +++ b/TracePrivately/Classes/DataManager.swift @@ -102,7 +102,8 @@ extension DataManager { } } - func saveInfectedKeys(keys: [ENTemporaryExposureKey], completion: @escaping (_ numNewKeys: Int, _ error: Swift.Error?) -> Void) { + + func saveInfectedKeys(keys: [TPTemporaryExposureKey], completion: @escaping (_ numNewKeys: Int, _ error: Swift.Error?) -> Void) { guard keys.count > 0 else { completion(0, nil) @@ -177,6 +178,7 @@ extension DataManager { entity.infectedKey = data // Core data doesn't support unsigned ints, so using Int64 instead of UInt32 entity.rollingStartNumber = Int64(key.rollingStartNumber) + entity.transmissionRiskLevel = Int16(key.transmissionRiskLevel.rawValue) numNewKeys += 1 } @@ -204,7 +206,7 @@ extension DataManager { } } - func allInfectedKeys(completion: @escaping ([ENTemporaryExposureKey]?, Swift.Error?) -> Void) { + func allInfectedKeys(completion: @escaping ([TPTemporaryExposureKey]?, Swift.Error?) -> Void) { let context = self.persistentContainer.newBackgroundContext() context.perform { @@ -213,7 +215,7 @@ extension DataManager { do { let entities = try context.fetch(request) - let keys: [ENTemporaryExposureKey] = entities.compactMap { $0.temporaryExposureKey } + let keys: [TPTemporaryExposureKey] = entities.compactMap { $0.temporaryExposureKey } completion(keys, nil) } catch { @@ -224,33 +226,39 @@ extension DataManager { } extension RemoteInfectedKeyEntity { - var temporaryExposureKey: ENTemporaryExposureKey? { + var temporaryExposureKey: TPTemporaryExposureKey? { guard let keyData = self.infectedKey else { return nil } - return ENTemporaryExposureKey( + let riskLevel: TPRiskLevel? = TPRiskLevel(rawValue: UInt8(self.transmissionRiskLevel)) + + return .init( keyData: keyData, - rollingStartNumber: ENIntervalNumber(self.rollingStartNumber) + rollingStartNumber: TPIntervalNumber(self.rollingStartNumber), + transmissionRiskLevel: riskLevel ?? .invalid ) } } extension LocalInfectionKeyEntity { - var temporaryExposureKey: ENTemporaryExposureKey? { + var temporaryExposureKey: TPTemporaryExposureKey? { guard let keyData = self.infectedKey else { return nil } - return ENTemporaryExposureKey( + let riskLevel: TPRiskLevel? = TPRiskLevel(rawValue: UInt8(self.transmissionRiskLevel)) + + return .init( keyData: keyData, - rollingStartNumber: ENIntervalNumber(self.rollingStartNumber) + rollingStartNumber: TPIntervalNumber(self.rollingStartNumber), + transmissionRiskLevel: riskLevel ?? .invalid ) } } extension DataManager { - func submitReport(formData: InfectedKeysFormData, keys: [ENTemporaryExposureKey], completion: @escaping (Bool, Swift.Error?) -> Void) { + func submitReport(formData: InfectedKeysFormData, keys: [TPTemporaryExposureKey], completion: @escaping (Bool, Swift.Error?) -> Void) { let context = self.persistentContainer.newBackgroundContext() context.perform { @@ -276,6 +284,7 @@ extension DataManager { let keyEntity = LocalInfectionKeyEntity(context: context) keyEntity.infectedKey = key.keyData keyEntity.rollingStartNumber = Int64(key.rollingStartNumber) + keyEntity.transmissionRiskLevel = Int16(key.transmissionRiskLevel.rawValue) keyEntity.infection = entity } @@ -317,14 +326,14 @@ extension DataManager { case sent = "S" } - func saveExposures(exposures: [ENExposureInfo], completion: @escaping (Error?) -> Void) { + func saveExposures(exposures: [TPExposureInfo], completion: @escaping (Error?) -> Void) { let context = self.persistentContainer.newBackgroundContext() context.perform { var delete: [ExposureContactInfoEntity] = [] - var insert: [ENExposureInfo] = [] + var insert: [TPExposureInfo] = [] do { let request = ExposureFetchRequest(includeStatuses: [], includeNotificationStatuses: [], sortDirection: nil) @@ -456,15 +465,23 @@ extension DataManager { } extension ExposureContactInfoEntity { - var contactInfo: ENExposureInfo? { + var contactInfo: TPExposureInfo? { guard let timestamp = self.timestamp else { return nil } - return ENExposureInfo(attenuationValue: UInt8(self.attenuationValue), date: timestamp, duration: self.duration) + let transmissionRiskLevel: TPRiskLevel? = TPRiskLevel(rawValue: UInt8(self.transmissionRiskLevel)) + + return .init( + attenuationValue: UInt8(self.attenuationValue), + date: timestamp, + duration: self.duration, + totalRiskScore: TPRiskScore(self.totalRiskScore), + transmissionRiskLevel: transmissionRiskLevel ?? .invalid + ) } - func matches(exposure: ENExposureInfo) -> Bool { + func matches(exposure: TPExposureInfo) -> Bool { if exposure.attenuationValue != UInt8(self.attenuationValue) { return false } diff --git a/TracePrivately/Classes/ENMockFramework.swift b/TracePrivately/Classes/ENMockFramework.swift index f81fb00..29b096d 100644 --- a/TracePrivately/Classes/ENMockFramework.swift +++ b/TracePrivately/Classes/ENMockFramework.swift @@ -6,8 +6,9 @@ import Foundation import UIKit -enum ENErrorCode { - case success +/// To use the real ExposureNotifications framework, just comment out this entire file. You must be running iOS 13.4 or newer + +enum ENErrorCode: Int { case unknown case badParameter case notEntitled @@ -18,12 +19,12 @@ enum ENErrorCode { case insufficientStorage case notEnabled case apiMisuse - case internalError + case `internal` case insufficientMemory + case rateLimited var localizedTitle: String { switch self { - case .success: return "Success" case .unknown: return "Unknown" case .badParameter: return "Bad Parameter" case .notEntitled: return "Not Entitled" @@ -34,17 +35,18 @@ enum ENErrorCode { case .insufficientStorage: return "Insufficient Storage" case .notEnabled: return "Not Enabled" case .apiMisuse: return "API Miuse" - case .internalError: return "Internal Error" + case .internal: return "Internal Error" case .insufficientMemory: return "Insufficient Memory" + case .rateLimited: return "Rate Limited" } } } struct ENError: LocalizedError { - let errorCode: ENErrorCode + let code: ENErrorCode var localizedDescription: String { - return errorCode.localizedTitle + return code.localizedTitle } } @@ -67,7 +69,7 @@ protocol ENActivatable { var dispatchQueue: DispatchQueue? { get set } var invalidationHandler: (() -> Void)? { get set } - func activateWithCompletion(_ completion: @escaping ENErrorHandler) + func activate(_ completionHandler: @escaping ENErrorHandler) func invalidate() } @@ -76,75 +78,50 @@ protocol ENAuthorizable { var authorizationMode: ENAuthorizationMode { get set } } -typealias ENMultiState = Bool +typealias ENIntervalNumber = UInt32 + +typealias ENAttenuation = UInt8 + +public enum ENRiskLevel : UInt8 { -class ENSettings { - let enableState: ENMultiState - init(enableState: ENMultiState) { - self.enableState = enableState - } -} + case invalid = 0 /// Invalid level. Used when it isn't available. -class ENMutableSettings: ENSettings { + /// Invalid level. Used when it isn't available. + case lowest = 1 /// Lowest risk. -} + /// Lowest risk. + case low = 10 /// Low risk. -class ENSettingsGetRequest: ENBaseRequest { - private var _settings: ENSettings? = nil - - var settings: ENSettings? { - get { - return enQueue.sync { - return self._settings - } - } - set { - enQueue.sync { - self._settings = newValue - } - } + /// Low risk. + case lowMedium = 25 /// Risk between low and medium. - } - - override fileprivate func activate(queue: DispatchQueue, completion: @escaping (Error?) -> Void) { - queue.async { - self.settings = ENSettings(enableState: ENInternalState.shared.tracingEnabled) - completion(nil) - } - } + /// Risk between low and medium. + case medium = 50 /// Medium risk. + + /// Medium risk. + case mediumHigh = 65 /// Risk between medium and high. + + /// Risk between medium and high. + case high = 80 /// High risk. + + /// High risk. + case veryHigh = 90 /// Very high risk. + + /// Very high risk. + case highest = 100 /// Highest risk. } -class ENSettingsChangeRequest: ENAuthorizableBaseRequest { - let settings: ENSettings - - override var permissionDialogMessage: String? { - return "Allow this app to detect exposures?" - } - - init(settings: ENSettings) { - self.settings = settings - } - - override var shouldPrompt: Bool { - return self.settings.enableState == true - } +class ENTemporaryExposureKey { + var keyData: Data! + var rollingStartNumber: ENIntervalNumber! + var transmissionRiskLevel: ENRiskLevel! - override fileprivate func activateWithPermission(queue: DispatchQueue, completion: @escaping (Error?) -> Void) { - queue.async { - ENInternalState.shared.tracingEnabled = self.settings.enableState - completion(nil) - } + init() { + } } -typealias ENIntervalNumber = UInt32 - -struct ENTemporaryExposureKey { - let keyData: Data - let rollingStartNumber: ENIntervalNumber -} - extension ENTemporaryExposureKey { // This is used so we can resolve a date from the key var ymd: DateComponents? { @@ -181,19 +158,178 @@ extension ENTemporaryExposureKey { } } +typealias ENGetDiagnosisKeysHandler = ([ENTemporaryExposureKey]?, Error?) -> Void + +enum ENStatus : Int { + + + /// Status of Exposure Notification is unknown. This is the status before ENManager has activated successfully. + case unknown = 0 + + + /// Exposure Notification is active on the system. + case active = 1 + + + /// Exposure Notification is disabled. setExposureNotificationEnabled:completionHandler can be used to enable it. + case disabled = 2 + + + /// Bluetooth has been turned off on the system. Bluetooth is required for Exposure Notification. + /// Note: this may not match the state of Bluetooth as reported by CoreBluetooth. + /// Exposure Notification is a system service and can use Bluetooth in situations when apps cannot. + /// So for the purposes of Exposure Notification, it's better to use this API instead of CoreBluetooth. + case bluetoothOff = 3 + + + /// Exposure Notification is not active due to system restrictions, such as parental controls. + /// When in this state, the app cannot enable Exposure Notification. + case restricted = 4 +} + + +class ENManager: ENBaseRequest { + + var exposureNotificationStatus: ENStatus { + // TODO: Implement properly + return .active + } + + func setExposureNotificationEnabled(_ flag: Bool, completion: @escaping ENErrorHandler) { + // TODO: Doesn't do anything + // TODO: Request permission here + completion(nil) + } + +// override var permissionDialogMessage: String? { +// return "Allow this app to retrieve your anonymous tracing keys?" +// } + + func getDiagnosisKeys(completionHandler: @escaping ENGetDiagnosisKeysHandler) { + + let delay: TimeInterval = 0.5 + + let queue = self.dispatchQueue ?? .main + + queue.asyncAfter(deadline: .now() + delay) { + completionHandler(ENInternalState.shared.dailyKeys, nil) + } + } + + func resetAllData(completionHandler: @escaping ENErrorHandler) { + // TODO: Show auth dialog here + completionHandler(nil) + } +} + +typealias ENRiskScore = UInt8 + struct ENExposureDetectionSummary { let daysSinceLastExposure: Int let matchedKeyCount: UInt64 + let maximumRiskScore: ENRiskScore // TODO: Make use of this. } typealias ENExposureDetectionFinishCompletion = ((ENExposureDetectionSummary?, Swift.Error?) -> Void) -typealias ENExposureDetectionGetExposureInfoCompletion = (([ENExposureInfo]?, Bool, Swift.Error?) -> Void) +typealias ENGetExposureInfoCompletion = (([ENExposureInfo]?, Bool, Swift.Error?) -> Void) + +class ENExposureConfiguration { + init() { + self.minimumRiskScore = 0 + self.attenuationScores = [] + self.attenuationWeight = 0 + self.daysSinceLastExposureScores = [] + self.daysSinceLastExposureWeight = 0 + self.durationScores = [] + self.durationWeight = 0 + self.transmissionRiskScores = [] + self.transmissionRiskWeight = 0 + } + + /// Minimum risk score. Excludes exposure incidents with scores lower than this. Defaults to no minimum. + var minimumRiskScore: ENRiskScore + + + //--------------------------------------------------------------------------------------------------------------------------- + /** @brief Scores for attenuation buckets. Must contain 8 scores, one for each bucket as defined below: + + attenuationScores[0] when Attenuation > 73. + attenuationScores[1] when 73 >= Attenuation > 63. + attenuationScores[2] when 63 >= Attenuation > 51. + attenuationScores[3] when 51 >= Attenuation > 33. + attenuationScores[4] when 33 >= Attenuation > 27. + attenuationScores[5] when 27 >= Attenuation > 15. + attenuationScores[6] when 15 >= Attenuation > 10. + attenuationScores[7] when 10 >= Attenuation. + */ + var attenuationScores: [NSNumber] + + + /// Weight to apply to the attenuation score. Must be in the range 0-100. + var attenuationWeight: Double + + + //--------------------------------------------------------------------------------------------------------------------------- + /** @brief Scores for days since last exposure buckets. Must contain 8 scores, one for each bucket as defined below: + + daysSinceLastExposureScores[0] when Days >= 14. + daysSinceLastExposureScores[1] else Days >= 12 + daysSinceLastExposureScores[2] else Days >= 10 + daysSinceLastExposureScores[3] else Days >= 8 + daysSinceLastExposureScores[4] else Days >= 6 + daysSinceLastExposureScores[5] else Days >= 4 + daysSinceLastExposureScores[6] else Days >= 2 + daysSinceLastExposureScores[7] else Days >= 0 + */ + var daysSinceLastExposureScores: [NSNumber] + + + /// Weight to apply to the days since last exposure score. Must be in the range 0-100. + var daysSinceLastExposureWeight: Double + + + //--------------------------------------------------------------------------------------------------------------------------- + /** @brief Scores for duration buckets. Must contain 8 scores, one for each bucket as defined below: + + durationScores[0] when Duration == 0 + durationScores[1] else Duration <= 5 + durationScores[2] else Duration <= 10 + durationScores[3] else Duration <= 15 + durationScores[4] else Duration <= 20 + durationScores[5] else Duration <= 25 + durationScores[6] else Duration <= 30 + durationScores[7] else Duration > 30 + */ + var durationScores: [NSNumber] + + + /// Weight to apply to the duration score. Must be in the range 0-100. + var durationWeight: Double + + + //--------------------------------------------------------------------------------------------------------------------------- + /** @brief Scores for transmission risk buckets. Must contain 8 scores, one for each bucket as defined below: + + transmissionRiskScores[0] for ENRiskLevelLowest. + transmissionRiskScores[1] for ENRiskLevelLow. + transmissionRiskScores[2] for ENRiskLevelLowMedium. + transmissionRiskScores[3] for ENRiskLevelMedium. + transmissionRiskScores[4] for ENRiskLevelMediumHigh. + transmissionRiskScores[5] for ENRiskLevelHigh. + transmissionRiskScores[6] for ENRiskLevelVeryHigh. + transmissionRiskScores[7] for ENRiskLevelHighest. + */ + var transmissionRiskScores: [NSNumber] + + + /// Weight to apply to the transmission risk score. Must be in the range 0-100. + var transmissionRiskWeight: Double +} class ENExposureDetectionSession: ENBaseRequest { - var attenuationThreshold: UInt8 = 0 - var durationThreshold: TimeInterval = 0 - var maxKeyCount: Int = 10 + var configuration = ENExposureConfiguration() + var maximumKeyCount: Int = 10 private var _infectedKeys: [ENTemporaryExposureKey] = [] @@ -209,7 +345,7 @@ class ENExposureDetectionSession: ENBaseRequest { } } - func addDiagnosisKeys(inKeys keys: [ENTemporaryExposureKey], completion: @escaping ENErrorHandler) { + func addDiagnosisKeys(_ keys: [ENTemporaryExposureKey], completionHandler: @escaping ENErrorHandler) { enQueue.sync { self._infectedKeys.append(contentsOf: keys) } @@ -217,11 +353,11 @@ class ENExposureDetectionSession: ENBaseRequest { let queue = self.dispatchQueue ?? .main queue.asyncAfter(deadline: .now() + 0.5) { - completion(nil) + completionHandler(nil) } } - func finishedDiagnosisKeysWithCompletion(completion: @escaping ENExposureDetectionFinishCompletion) { + func finishedDiagnosisKeys(completionHandler: @escaping ENExposureDetectionFinishCompletion) { let delay: TimeInterval = 0.5 @@ -232,17 +368,18 @@ class ENExposureDetectionSession: ENBaseRequest { let summary = ENExposureDetectionSummary( daysSinceLastExposure: 0, - matchedKeyCount: UInt64(min(Self.maximumFakeMatches, keys.count)) + matchedKeyCount: UInt64(min(Self.maximumFakeMatches, keys.count)), + maximumRiskScore: 8 ) - completion(summary, nil) + completionHandler(summary, nil) } } private var cursor: Int = 0 - func getExposureInfoWithMaxCount(maxCount: UInt32, completion: @escaping ENExposureDetectionGetExposureInfoCompletion) { + func getExposureInfo(withMaximumCount maximumCount: Int, completionHandler: @escaping ENGetExposureInfoCompletion) { let queue: DispatchQueue = self.dispatchQueue ?? .main @@ -251,7 +388,7 @@ class ENExposureDetectionSession: ENBaseRequest { queue.asyncAfter(deadline: .now() + delay) { guard !self.isInvalidated else { self.cursor = 0 - completion(nil, true, ENError(errorCode: .invalidated)) + completionHandler(nil, true, ENError(code: .invalidated)) return } @@ -259,18 +396,18 @@ class ENExposureDetectionSession: ENBaseRequest { let allKeys: [ENTemporaryExposureKey] = enQueue.sync { self.remoteInfectedKeys } guard allKeys.count > 0 else { - completion([], true, nil) + completionHandler([], true, nil) return } let allMatchedKeys: [ENTemporaryExposureKey] = Array(allKeys[0 ..< min(Self.maximumFakeMatches, allKeys.count)]) let fromIndex = self.cursor - let toIndex = min(allMatchedKeys.count, self.cursor + Int(maxCount)) + let toIndex = min(allMatchedKeys.count, self.cursor + Int(maximumCount)) guard fromIndex < toIndex else { self.cursor = 0 - completion([], true, nil) + completionHandler([], true, nil) return } @@ -281,59 +418,29 @@ class ENExposureDetectionSession: ENBaseRequest { let date = Date(timeIntervalSince1970: TimeInterval(key.rollingStartNumber * 600)) let duration: TimeInterval = 15 * 60 - return ENExposureInfo(attenuationValue: 0, date: date, duration: duration) + return ENExposureInfo( + attenuationValue: 0, + date: date, + duration: duration, + totalRiskScore: 51, + transmissionRiskLevel: .medium + ) } let inDone = toIndex >= allMatchedKeys.count self.cursor = inDone ? 0 : toIndex - completion(contacts, inDone, nil) + completionHandler(contacts, inDone, nil) } } } struct ENExposureInfo { - let attenuationValue: UInt8 + let attenuationValue: ENAttenuation let date: Date let duration: TimeInterval -} - -struct ENSelfExposureInfo { - let keys: [ENTemporaryExposureKey] -} - -class ENSelfExposureInfoRequest: ENAuthorizableBaseRequest { - private var _selfExposureInfo: ENSelfExposureInfo? - - var selfExposureInfo: ENSelfExposureInfo? { - get { - return enQueue.sync { - return self._selfExposureInfo - } - } - set { - enQueue.sync { - self._selfExposureInfo = newValue - } - } - } - - override var permissionDialogMessage: String? { - return "Allow this app to retrieve your anonymous tracing keys?" - } - - override fileprivate func activateWithPermission(queue: DispatchQueue, completion: @escaping (Error?) -> Void) { - - let delay: TimeInterval = 0.5 - - queue.asyncAfter(deadline: .now() + delay) { - - let info = ENSelfExposureInfo(keys: ENInternalState.shared.dailyKeys) - self.selfExposureInfo = info - - completion(nil) - } - } + let totalRiskScore: ENRiskScore + let transmissionRiskLevel: ENRiskLevel } class ENSelfExposureResetRequest: ENAuthorizableBaseRequest { @@ -342,14 +449,14 @@ class ENSelfExposureResetRequest: ENAuthorizableBaseRequest { return "Allow this app to reset your anonymous tracing keys?" } - override fileprivate func activateWithPermission(queue: DispatchQueue, completion: @escaping (Error?) -> Void) { + override fileprivate func activateWithPermission(queue: DispatchQueue, completionHandler: @escaping (Error?) -> Void) { print("Resetting keys ...") queue.asyncAfter(deadline: .now() + 0.5) { print("Finished resetting keys") // Nothing to do since we're generating fake stable keys for the purpose of testing - completion(nil) + completionHandler(nil) } } } @@ -369,7 +476,7 @@ class ENAuthorizableBaseRequest: ENBaseRequest, ENAuthorizable { return true } - final override fileprivate func activate(queue: DispatchQueue, completion: @escaping (Error?) -> Void) { + final override fileprivate func activate(queue: DispatchQueue, completionHandler: @escaping (Error?) -> Void) { if self.shouldPrompt { DispatchQueue.main.async { @@ -379,16 +486,16 @@ class ENAuthorizableBaseRequest: ENBaseRequest, ENAuthorizable { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Deny", style: .cancel, handler: { action in - completion(ENError(errorCode: .notAuthorized)) + completionHandler(ENError(code: .notAuthorized)) return })) alert.addAction(UIAlertAction(title: "Allow", style: .default, handler: { action in - self.activateWithPermission(queue: queue, completion: completion) + self.activateWithPermission(queue: queue, completionHandler: completionHandler) })) guard let vc = UIApplication.shared.windows.first?.rootViewController else { - completion(ENError(errorCode: .unknown)) + completionHandler(ENError(code: .unknown)) return } @@ -402,14 +509,14 @@ class ENAuthorizableBaseRequest: ENBaseRequest, ENAuthorizable { } else { let queue = self.dispatchQueue ?? .main - self.activateWithPermission(queue: queue, completion: completion) + self.activateWithPermission(queue: queue, completionHandler: completionHandler) } } - fileprivate func activateWithPermission(queue: DispatchQueue, completion: @escaping (Error?) -> Void) { + fileprivate func activateWithPermission(queue: DispatchQueue, completionHandler: @escaping (Error?) -> Void) { queue.async { print("Should be overridden") - completion(nil) + completionHandler(nil) } } } @@ -463,26 +570,26 @@ class ENBaseRequest: ENActivatable { } } - final func activateWithCompletion(_ completion: @escaping (Swift.Error?) -> Void) { + final func activate(_ completionHandler: @escaping (Swift.Error?) -> Void) { let queue: DispatchQueue = self.dispatchQueue ?? .main self.isRunning = true self.activate(queue: queue) { error in guard !self.isInvalidated else { - completion(ENError(errorCode: .invalidated)) + completionHandler(ENError(code: .invalidated)) return } self.isRunning = false - completion(error) + completionHandler(error) } } - fileprivate func activate(queue: DispatchQueue, completion: @escaping (Swift.Error?) -> Void) { + fileprivate func activate(queue: DispatchQueue, completionHandler: @escaping (Swift.Error?) -> Void) { queue.async { print("Should be overridden") - completion(nil) + completionHandler(nil) } } @@ -567,7 +674,14 @@ private class ENInternalState { let intervalNumber = ENIntervalNumber(date.timeIntervalSince1970 / 600) let rollingStartNumber = intervalNumber / 144 * 144 - keys.append(ENTemporaryExposureKey(keyData: keyData, rollingStartNumber: rollingStartNumber)) + + + let key = ENTemporaryExposureKey() + key.keyData = keyData + key.rollingStartNumber = rollingStartNumber + key.transmissionRiskLevel = .high // TODO: Make better use of risk level + + keys.append(key) } print("Generated keys: \(keys)") @@ -595,3 +709,4 @@ extension String { } } } + diff --git a/TracePrivately/Classes/ExposureDataTypes.swift b/TracePrivately/Classes/ExposureDataTypes.swift new file mode 100644 index 0000000..a6592f7 --- /dev/null +++ b/TracePrivately/Classes/ExposureDataTypes.swift @@ -0,0 +1,57 @@ +// +// ExposureDataTypes.swift +// TracePrivately +// + +import Foundation +#if canImport(ExposureNotification) +import ExposureNotification +#endif + +// These types map to the ExposureNotification framework so it can easily be factored out + +typealias TPIntervalNumber = ENIntervalNumber +typealias TPAttenuation = ENAttenuation +typealias TPRiskScore = ENRiskScore +typealias TPRiskLevel = ENRiskLevel + +struct TPTemporaryExposureKey { + let keyData: Data + let rollingStartNumber: TPIntervalNumber + let transmissionRiskLevel: TPRiskLevel! +} + +extension TPTemporaryExposureKey { + var enExposureKey: ENTemporaryExposureKey { + let key = ENTemporaryExposureKey() + key.keyData = keyData + key.rollingStartNumber = rollingStartNumber + key.transmissionRiskLevel = transmissionRiskLevel + + return key + } +} + +extension ENTemporaryExposureKey { + var tpExposureKey: TPTemporaryExposureKey { + return .init( + keyData: keyData, + rollingStartNumber: rollingStartNumber, + transmissionRiskLevel: transmissionRiskLevel + ) + } +} + +struct TPExposureInfo { + let attenuationValue: TPAttenuation + let date: Date + let duration: TimeInterval + let totalRiskScore: TPRiskScore + let transmissionRiskLevel: TPRiskLevel +} + +extension ENExposureInfo { + var tpExposureInfo: TPExposureInfo { + return .init(attenuationValue: attenuationValue, date: date, duration: duration, totalRiskScore: totalRiskScore, transmissionRiskLevel: transmissionRiskLevel) + } +} diff --git a/TracePrivately/Classes/KeyServer/KeyServer.swift b/TracePrivately/Classes/KeyServer/KeyServer.swift index 968b602..dcc1552 100644 --- a/TracePrivately/Classes/KeyServer/KeyServer.swift +++ b/TracePrivately/Classes/KeyServer/KeyServer.swift @@ -191,7 +191,7 @@ extension KeyServer { Refer to `KeyServer.yaml` for expected request and response format. */ - func submitInfectedKeys(formData: InfectedKeysFormData, keys: [ENTemporaryExposureKey], previousSubmissionId: String?, completion: @escaping (Bool, String?, Swift.Error?) -> Void) { + func submitInfectedKeys(formData: InfectedKeysFormData, keys: [TPTemporaryExposureKey], previousSubmissionId: String?, completion: @escaping (Bool, String?, Swift.Error?) -> Void) { self._submitInfectedKeys(formData: formData, keys: keys, previousSubmissionId: previousSubmissionId) { success, submissionId, error in if let error = error as? KeyServer.Error, error.shouldRetryWithAuthRequest { @@ -212,7 +212,7 @@ extension KeyServer { } } - private func _submitInfectedKeys(formData: InfectedKeysFormData, keys: [ENTemporaryExposureKey], previousSubmissionId: String?, completion: @escaping (Bool, String?, Swift.Error?) -> Void) { + private func _submitInfectedKeys(formData: InfectedKeysFormData, keys: [TPTemporaryExposureKey], previousSubmissionId: String?, completion: @escaping (Bool, String?, Swift.Error?) -> Void) { guard let endPoint = self.config.submitInfected else { completion(false, nil, Error.invalidConfig) @@ -225,7 +225,8 @@ extension KeyServer { let encodedKeys: [[String: Any]] = keys.map { key in return [ "d": key.keyData.base64EncodedString(), - "r": key.rollingStartNumber + "r": key.rollingStartNumber, + "l": key.transmissionRiskLevel.rawValue ] } @@ -310,8 +311,8 @@ extension KeyServer { struct InfectedKeysResponse { let date: Date - let keys: [ENTemporaryExposureKey] - let deletedKeys: [ENTemporaryExposureKey] + let keys: [TPTemporaryExposureKey] + let deletedKeys: [TPTemporaryExposureKey] } func retrieveInfectedKeys(since date: Date?, completion: @escaping (InfectedKeysResponse?, Swift.Error?) -> Void) { @@ -398,7 +399,7 @@ extension KeyServer { return } - let keys: [ENTemporaryExposureKey] = keysData.compactMap { ENTemporaryExposureKey(jsonData: $0) } + let keys: [TPTemporaryExposureKey] = keysData.compactMap { TPTemporaryExposureKey(jsonData: $0) } print("Found \(keys.count) key(s)") @@ -415,7 +416,8 @@ extension KeyServer { } } -extension ENTemporaryExposureKey { +// TODO: Ensure server supports this value +extension TPTemporaryExposureKey { init?(jsonData: [String: Any]) { guard let base64str = jsonData["d"] as? String, let keyData = Data(base64Encoded: base64str) else { return nil @@ -425,11 +427,24 @@ extension ENTemporaryExposureKey { return nil } - self.init(keyData: keyData, rollingStartNumber: rollingStartNumber) + let riskLevel: TPRiskLevel? + + if let val = jsonData["l"] as? UInt8 { + riskLevel = TPRiskLevel(rawValue: val) + } + else { + riskLevel = nil + } + + self.init( + keyData: keyData, + rollingStartNumber: rollingStartNumber, + transmissionRiskLevel: riskLevel ?? .invalid + ) } } -extension ENTemporaryExposureKey { +extension TPTemporaryExposureKey { // TODO: Implement this so data can be read off the wire in binary format // init?(networkData: Data) { // diff --git a/TracePrivately/Classes/View Controllers/ExposedViewController.swift b/TracePrivately/Classes/View Controllers/ExposedViewController.swift index 20a8334..a7688fe 100644 --- a/TracePrivately/Classes/View Controllers/ExposedViewController.swift +++ b/TracePrivately/Classes/View Controllers/ExposedViewController.swift @@ -19,7 +19,7 @@ class ExposedViewController: UICollectionViewController { } enum CellType { - case contact(ENExposureInfo) + case contact(TPExposureInfo) case intro(String) case nextSteps } @@ -70,7 +70,7 @@ class ExposedViewController: UICollectionViewController { entities = [] } - let contacts: [ENExposureInfo] = entities.compactMap { $0.contactInfo } + let contacts: [TPExposureInfo] = entities.compactMap { $0.contactInfo } if contacts.count == 0 { let title = String(format: NSLocalizedString("exposure.none.message", comment: ""), Disease.current.localizedTitle) diff --git a/TracePrivately/Classes/View Controllers/MainViewController.swift b/TracePrivately/Classes/View Controllers/MainViewController.swift index 9c2d75f..39a72f9 100644 --- a/TracePrivately/Classes/View Controllers/MainViewController.swift +++ b/TracePrivately/Classes/View Controllers/MainViewController.swift @@ -174,7 +174,13 @@ class MainViewController: UIViewController { self.submitInfectionContainer.backgroundColor = color } else { - self.view.backgroundColor = .groupTableViewBackground + if #available(iOS 13, *) { + self.view.backgroundColor = .systemGroupedBackground + } + else { + self.view.backgroundColor = .groupTableViewBackground + } + self.tracingContainer.backgroundColor = .white self.submitInfectionContainer.backgroundColor = .white } @@ -278,18 +284,18 @@ extension MainViewController { let haptics = UINotificationFeedbackGenerator() haptics.notificationOccurred(.success) - + ContactTraceManager.shared.startTracing { error in if let error = error { - if let error = error as? ENError, error.errorCode == .notAuthorized { - - } - else { - let alert = UIAlertController(title: NSLocalizedString("error", comment: ""), message: error.localizedDescription, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("ok", comment: ""), style: .default, handler: nil)) - - self.present(alert, animated: true, completion: nil) - } + print("Error: \(error)") + + // Will show alert on permission denied as it's not possible to tell if the user made the decision now or earlier. + // Perhaps include instruction on how to resolve this + + let alert = UIAlertController(title: NSLocalizedString("error", comment: ""), message: error.localizedDescription, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("ok", comment: ""), style: .default, handler: nil)) + + self.present(alert, animated: true, completion: nil) } } } diff --git a/TracePrivately/Classes/View Controllers/SettingsViewController.swift b/TracePrivately/Classes/View Controllers/SettingsViewController.swift index 5878354..7b36e1c 100644 --- a/TracePrivately/Classes/View Controllers/SettingsViewController.swift +++ b/TracePrivately/Classes/View Controllers/SettingsViewController.swift @@ -37,13 +37,7 @@ extension SettingsViewController { alert.addAction(UIAlertAction(title: NSLocalizedString("reset_keys.button.title", comment: ""), style: .destructive, handler: { _ in - let request = ENSelfExposureResetRequest() - - request.activateWithCompletion { _ in - defer { - request.invalidate() - } - + ContactTraceManager.shared.resetAllData { _ in DataManager.shared.deleteLocalInfections { _ in DispatchQueue.main.async { self.dismiss(animated: true, completion: nil) diff --git a/TracePrivately/Classes/View Controllers/SubmitInfectionViewController.swift b/TracePrivately/Classes/View Controllers/SubmitInfectionViewController.swift index e68fa98..ce69657 100644 --- a/TracePrivately/Classes/View Controllers/SubmitInfectionViewController.swift +++ b/TracePrivately/Classes/View Controllers/SubmitInfectionViewController.swift @@ -251,9 +251,6 @@ extension SubmitInfectionViewController { func createFormElement(field: SubmitInfectionConfig.Field) -> SubmitInfectionFormContainerView? { - let isDarkMode = self.isDarkMode - - var headingSubViews: [UIView] = [] var bodySubViews: [UIView] = [] @@ -404,45 +401,26 @@ extension SubmitInfectionViewController { let haptics = UINotificationFeedbackGenerator() haptics.notificationOccurred(.success) - - let request = ENSelfExposureInfoRequest() - request.activateWithCompletion { error in - defer { - request.invalidate() - } - - guard let exposureInfo = request.selfExposureInfo else { - var showError = true - - if let error = error as? ENError { - switch error.errorCode { - case .notAuthorized: - showError = false - default: - break - } - } - - if showError { + ContactTraceManager.shared.retrieveSelfDiagnosisKeys { keys, error in + DispatchQueue.main.async { + guard let keys = keys else { self.presentErrorAlert(title: nil, message: error?.localizedDescription ?? NSLocalizedString("infection.report.gathering_data.error", comment: "")) + + completion(false, error) + return } - completion(false, error) - return - } - - let keys = exposureInfo.keys - - let alert = UIAlertController(title: NSLocalizedString("infection.report.submit.title", comment: ""), message: NSLocalizedString("infection.report.submit.message", comment: ""), preferredStyle: .alert) - - alert.addAction(UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .cancel, handler: nil)) - alert.addAction(UIAlertAction(title: NSLocalizedString("submit", comment: ""), style: .destructive, handler: { action in + let alert = UIAlertController(title: NSLocalizedString("infection.report.submit.title", comment: ""), message: NSLocalizedString("infection.report.submit.message", comment: ""), preferredStyle: .alert) - self.submitReport(keys: keys, completion: completion) - })) - - self.present(alert, animated: true, completion: nil) + alert.addAction(UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .cancel, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("submit", comment: ""), style: .destructive, handler: { action in + + self.submitReport(keys: keys, completion: completion) + })) + + self.present(alert, animated: true, completion: nil) + } } } } @@ -490,7 +468,7 @@ extension SubmitInfectionViewController { } extension SubmitInfectionViewController { - func submitReport(keys: [ENTemporaryExposureKey], completion: @escaping (Bool, Swift.Error?) -> Void) { + func submitReport(keys: [TPTemporaryExposureKey], completion: @escaping (Bool, Swift.Error?) -> Void) { let formData = self.gatherFormData diff --git a/TracePrivately/KeyServer.plist b/TracePrivately/KeyServer.plist index 0bcf063..c43a902 100644 --- a/TracePrivately/KeyServer.plist +++ b/TracePrivately/KeyServer.plist @@ -3,7 +3,7 @@ BaseUrl - https://example.com/api/ + https://trace.crunchybagel.dev/api/ EndPoints GetInfectedKeys diff --git a/TracePrivately/TracePrivately.entitlements b/TracePrivately/TracePrivately.entitlements new file mode 100644 index 0000000..95c1713 --- /dev/null +++ b/TracePrivately/TracePrivately.entitlements @@ -0,0 +1,7 @@ + + + + + + + diff --git a/TracePrivately/TracePrivately.xcdatamodeld/.xccurrentversion b/TracePrivately/TracePrivately.xcdatamodeld/.xccurrentversion index f5b3fac..e46b68c 100644 --- a/TracePrivately/TracePrivately.xcdatamodeld/.xccurrentversion +++ b/TracePrivately/TracePrivately.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Model 7.xcdatamodel + Model 8.xcdatamodel diff --git a/TracePrivately/TracePrivately.xcdatamodeld/Model 8.xcdatamodel/contents b/TracePrivately/TracePrivately.xcdatamodeld/Model 8.xcdatamodel/contents new file mode 100644 index 0000000..6682952 --- /dev/null +++ b/TracePrivately/TracePrivately.xcdatamodeld/Model 8.xcdatamodel/contents @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file