mirror of
https://github.com/AU-COVIDSafe/mobile-ios.git
synced 2025-01-18 08:46:34 +00:00
Herald debug code for tech community
This commit is contained in:
parent
cf93ea43c0
commit
07fe747d48
63 changed files with 4448 additions and 1064 deletions
|
@ -36,6 +36,38 @@
|
|||
3A8951FDED310D7ABA54259E /* Pods_CovidSafe.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34E9EB96EA6E42A571E73C8E /* Pods_CovidSafe.framework */; };
|
||||
5904A5C22462471A008C8012 /* EncounterV1toV2.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 5904A5C12462471A008C8012 /* EncounterV1toV2.xcmappingmodel */; };
|
||||
5904A5C32462471A008C8012 /* EncounterV1toV2.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 5904A5C12462471A008C8012 /* EncounterV1toV2.xcmappingmodel */; };
|
||||
5905460C2543E0F5009B82AD /* BLESensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590545EE2543E0F5009B82AD /* BLESensor.swift */; };
|
||||
5905460D2543E0F5009B82AD /* BLESensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590545EE2543E0F5009B82AD /* BLESensor.swift */; };
|
||||
5905460E2543E0F5009B82AD /* BLEDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590545EF2543E0F5009B82AD /* BLEDatabase.swift */; };
|
||||
5905460F2543E0F5009B82AD /* BLEDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590545EF2543E0F5009B82AD /* BLEDatabase.swift */; };
|
||||
590546102543E0F5009B82AD /* BLEUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590545F02543E0F5009B82AD /* BLEUtilities.swift */; };
|
||||
590546112543E0F5009B82AD /* BLEUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590545F02543E0F5009B82AD /* BLEUtilities.swift */; };
|
||||
590546122543E0F6009B82AD /* BLETransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590545F12543E0F5009B82AD /* BLETransmitter.swift */; };
|
||||
590546132543E0F6009B82AD /* BLETransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590545F12543E0F5009B82AD /* BLETransmitter.swift */; };
|
||||
590546142543E0F6009B82AD /* BLEReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590545F22543E0F5009B82AD /* BLEReceiver.swift */; };
|
||||
590546152543E0F6009B82AD /* BLEReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590545F22543E0F5009B82AD /* BLEReceiver.swift */; };
|
||||
590546162543E0F6009B82AD /* AwakeSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590545F42543E0F5009B82AD /* AwakeSensor.swift */; };
|
||||
590546172543E0F6009B82AD /* AwakeSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590545F42543E0F5009B82AD /* AwakeSensor.swift */; };
|
||||
590546182543E0F6009B82AD /* Sensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590545F52543E0F5009B82AD /* Sensor.swift */; };
|
||||
590546192543E0F6009B82AD /* Sensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590545F52543E0F5009B82AD /* Sensor.swift */; };
|
||||
590546282543E0F6009B82AD /* SensorArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546012543E0F5009B82AD /* SensorArray.swift */; };
|
||||
590546292543E0F6009B82AD /* SensorArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546012543E0F5009B82AD /* SensorArray.swift */; };
|
||||
5905462A2543E0F6009B82AD /* PayloadDataSupplier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546022543E0F5009B82AD /* PayloadDataSupplier.swift */; };
|
||||
5905462B2543E0F6009B82AD /* PayloadDataSupplier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546022543E0F5009B82AD /* PayloadDataSupplier.swift */; };
|
||||
5905462C2543E0F6009B82AD /* TextFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546042543E0F5009B82AD /* TextFile.swift */; };
|
||||
5905462D2543E0F6009B82AD /* TextFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546042543E0F5009B82AD /* TextFile.swift */; };
|
||||
5905462E2543E0F6009B82AD /* BatteryLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546052543E0F5009B82AD /* BatteryLog.swift */; };
|
||||
5905462F2543E0F6009B82AD /* BatteryLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546052543E0F5009B82AD /* BatteryLog.swift */; };
|
||||
590546302543E0F6009B82AD /* DetectionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546062543E0F5009B82AD /* DetectionLog.swift */; };
|
||||
590546312543E0F6009B82AD /* DetectionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546062543E0F5009B82AD /* DetectionLog.swift */; };
|
||||
590546322543E0F6009B82AD /* SensorLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546072543E0F5009B82AD /* SensorLogger.swift */; };
|
||||
590546332543E0F6009B82AD /* SensorLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546072543E0F5009B82AD /* SensorLogger.swift */; };
|
||||
590546342543E0F6009B82AD /* ContactLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546082543E0F5009B82AD /* ContactLog.swift */; };
|
||||
590546352543E0F6009B82AD /* ContactLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546082543E0F5009B82AD /* ContactLog.swift */; };
|
||||
590546362543E0F6009B82AD /* StatisticsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546092543E0F5009B82AD /* StatisticsLog.swift */; };
|
||||
590546372543E0F6009B82AD /* StatisticsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590546092543E0F5009B82AD /* StatisticsLog.swift */; };
|
||||
590546382543E0F6009B82AD /* SensorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5905460A2543E0F5009B82AD /* SensorDelegate.swift */; };
|
||||
590546392543E0F6009B82AD /* SensorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5905460A2543E0F5009B82AD /* SensorDelegate.swift */; };
|
||||
590888AF2431B9E3008C9B9F /* UITextView + Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B55E1912430760500C9E798 /* UITextView + Extensions.swift */; };
|
||||
590888B02431B9E7008C9B9F /* UIColor + Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC141AD2430685800399FA8 /* UIColor + Extensions.swift */; };
|
||||
590888B12431B9EB008C9B9F /* Question3ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B86119424303F5E00EA4B6B /* Question3ViewController.swift */; };
|
||||
|
@ -167,12 +199,11 @@
|
|||
5B92D67F243018040049877B /* PogoInstructionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F19B4DC23F565850071A11E /* PogoInstructionsViewController.swift */; };
|
||||
5B92D680243018040049877B /* CodeInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E91BEA23EFEA0B002D592A /* CodeInputView.swift */; };
|
||||
5B92D681243018040049877B /* EncounterRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BE1CAE23F1349F005DCE4F /* EncounterRecord.swift */; };
|
||||
5B92D683243018040049877B /* OnboardingStep2bViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C58D817623F169DB00345771 /* OnboardingStep2bViewController.swift */; };
|
||||
5B92D683243018040049877B /* RegistrationSuccessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C58D817623F169DB00345771 /* RegistrationSuccessViewController.swift */; };
|
||||
5B92D684243018040049877B /* AsyncAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767D2F9242DF1B000DC9E2A /* AsyncAction.swift */; };
|
||||
5B92D685243018040049877B /* UILabelExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60F8BE3242659810007A641 /* UILabelExtension.swift */; };
|
||||
5B92D686243018040049877B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DD41D3A23DCB03B00FD4AB0 /* AppDelegate.swift */; };
|
||||
5B92D687243018040049877B /* PhoneNumberViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D8B248A23F146C500DBB74D /* PhoneNumberViewController.swift */; };
|
||||
5B92D688243018040049877B /* BluetraceUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DC7B8D2242B8D87008E1715 /* BluetraceUtils.swift */; };
|
||||
5B92D689243018040049877B /* NewFeedbackFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767D302242DF1B000DC9E2A /* NewFeedbackFlowController.swift */; };
|
||||
5B92D68A243018040049877B /* Outcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767D2EE242DF1B000DC9E2A /* Outcome.swift */; };
|
||||
5B92D68B243018040049877B /* Encounter+EncounterRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BE1CB023F134D0005DCE4F /* Encounter+EncounterRecord.swift */; };
|
||||
|
@ -184,9 +215,8 @@
|
|||
5B92D691243018040049877B /* RespondToAuthChallengeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB12C4C0242F047F007E893B /* RespondToAuthChallengeAPI.swift */; };
|
||||
5B92D692243018040049877B /* BluetraceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D269C0C23E2958F00ADF2DE /* BluetraceManager.swift */; };
|
||||
5B92D693243018040049877B /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767D2F0242DF1B000DC9E2A /* UIColor+Hex.swift */; };
|
||||
5B92D694243018040049877B /* OnboardingStep1ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5046D5B23EF18600046E96D /* OnboardingStep1ViewController.swift */; };
|
||||
5B92D694243018040049877B /* RegistrationIntroViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5046D5B23EF18600046E96D /* RegistrationIntroViewController.swift */; };
|
||||
5B92D695243018040049877B /* DeviceInfoExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767D2F5242DF1B000DC9E2A /* DeviceInfoExtension.swift */; };
|
||||
5B92D697243018040049877B /* PeripheralController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DD41D5223DD4CA400FD4AB0 /* PeripheralController.swift */; };
|
||||
5B92D698243018040049877B /* SendFeedbackAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767D2FA242DF1B000DC9E2A /* SendFeedbackAction.swift */; };
|
||||
5B92D699243018040049877B /* UITextViewFixed.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5D209FB23F4476F007233BE /* UITextViewFixed.swift */; };
|
||||
5B92D69A243018040049877B /* GradientButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C585C83A23EEB99B0061B7C6 /* GradientButton.swift */; };
|
||||
|
@ -198,9 +228,8 @@
|
|||
5B92D6A1243018040049877B /* AlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767D2F6242DF1B000DC9E2A /* AlertController.swift */; };
|
||||
5B92D6A2243018040049877B /* GetTempIdAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB12C4C2242F0FE9007E893B /* GetTempIdAPI.swift */; };
|
||||
5B92D6A3243018040049877B /* PhoneValidationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BFFD94A242EC120003AEF4F /* PhoneValidationAPI.swift */; };
|
||||
5B92D6A4243018040049877B /* CentralController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DD41D4E23DCB05600FD4AB0 /* CentralController.swift */; };
|
||||
5B92D6A5243018040049877B /* UIWindow+TopMost.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767D305242DF1B000DC9E2A /* UIWindow+TopMost.swift */; };
|
||||
5B92D6A6243018040049877B /* OnboardingStep1bViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DEB6812423AE2E00D99925 /* OnboardingStep1bViewController.swift */; };
|
||||
5B92D6A6243018040049877B /* HowItWorksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DEB6812423AE2E00D99925 /* HowItWorksViewController.swift */; };
|
||||
5B92D6A7243018040049877B /* OnboardingStep4ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C56CF43E23F18A15006B05B0 /* OnboardingStep4ViewController.swift */; };
|
||||
5B92D6A8243018040049877B /* PresentFeedbackExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767D2FD242DF1B000DC9E2A /* PresentFeedbackExtensions.swift */; };
|
||||
5B92D6A9243018040049877B /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767D304242DF1B000DC9E2A /* Errors.swift */; };
|
||||
|
@ -210,7 +239,7 @@
|
|||
5B92D6AD243018040049877B /* FeedbackSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767D301242DF1B000DC9E2A /* FeedbackSettings.swift */; };
|
||||
5B92D6AE243018040049877B /* JMCTargetJSONFromDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767D2EF242DF1B000DC9E2A /* JMCTargetJSONFromDisk.swift */; };
|
||||
5B92D6B0243018040049877B /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767D309242DF1B000DC9E2A /* Action.swift */; };
|
||||
5B92D6B1243018040049877B /* OnboardingStep2ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2F0BA123EFFF75006D7404 /* OnboardingStep2ViewController.swift */; };
|
||||
5B92D6B1243018040049877B /* AppSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2F0BA123EFFF75006D7404 /* AppSettingsViewController.swift */; };
|
||||
5B92D6B2243018040049877B /* HTTPPostFeedbackAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767D2F4242DF1B000DC9E2A /* HTTPPostFeedbackAction.swift */; };
|
||||
5B92D6B3243018040049877B /* PushNotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F36305E23F7F81400CC6E1D /* PushNotificationConstants.swift */; };
|
||||
5B92D6B4243018040049877B /* UIViewController + Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B615C5A823FA403400345969 /* UIViewController + Extension.swift */; };
|
||||
|
@ -242,6 +271,11 @@
|
|||
5BA33A7E24B55E7D00D12515 /* BLELogRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA33A7D24B55E7D00D12515 /* BLELogRecord.swift */; };
|
||||
5BA33A8024B55EF200D12515 /* BLELogDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA33A7F24B55EF200D12515 /* BLELogDB.swift */; };
|
||||
5BA33A8224B5602500D12515 /* BLELogRecord+BLELogSave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA33A8124B5602500D12515 /* BLELogRecord+BLELogSave.swift */; };
|
||||
5BBC571D25526F99005E90AA /* staging-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5BBC571C25526F99005E90AA /* staging-Info.plist */; };
|
||||
5BBE61B125633B6D00B8C983 /* CSGenericContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5BBE61B025633B6D00B8C983 /* CSGenericContentView.xib */; };
|
||||
5BBE61B225633B6D00B8C983 /* CSGenericContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5BBE61B025633B6D00B8C983 /* CSGenericContentView.xib */; };
|
||||
5BBE61B725633E8D00B8C983 /* CSGenericViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBE61B625633E8D00B8C983 /* CSGenericViewController.swift */; };
|
||||
5BBE61B825633E8D00B8C983 /* CSGenericViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBE61B625633E8D00B8C983 /* CSGenericViewController.swift */; };
|
||||
5BD3EE8324330E1A0004A007 /* UploadDataStep2VC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD3EE8224330E1A0004A007 /* UploadDataStep2VC.swift */; };
|
||||
5BD3EE84243313450004A007 /* UploadDataStep2VC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD3EE8224330E1A0004A007 /* UploadDataStep2VC.swift */; };
|
||||
5BED1E3A24A96D1C0066C4D2 /* LaunchScreen_ar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5BED1E3924A96D1C0066C4D2 /* LaunchScreen_ar.storyboard */; };
|
||||
|
@ -264,15 +298,12 @@
|
|||
5D8DD06223E319B300E097EF /* Encounter+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D8DD06023E319B300E097EF /* Encounter+CoreDataProperties.swift */; };
|
||||
5D8DD06723E329A200E097EF /* tracer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5DD41D7723DE141700FD4AB0 /* tracer.xcdatamodeld */; };
|
||||
5DC7B8D1242B8536008E1715 /* BluetraceConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DC7B8D0242B8536008E1715 /* BluetraceConfig.swift */; };
|
||||
5DC7B8D3242B8D87008E1715 /* BluetraceUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DC7B8D2242B8D87008E1715 /* BluetraceUtils.swift */; };
|
||||
5DD41D3B23DCB03B00FD4AB0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DD41D3A23DCB03B00FD4AB0 /* AppDelegate.swift */; };
|
||||
5DD41D4223DCB03B00FD4AB0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5DD41D4023DCB03B00FD4AB0 /* Main.storyboard */; };
|
||||
5DD41D4423DCB03D00FD4AB0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5DD41D4323DCB03D00FD4AB0 /* Assets.xcassets */; };
|
||||
5DD41D4723DCB03D00FD4AB0 /* LaunchScreen_en.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5DD41D4523DCB03D00FD4AB0 /* LaunchScreen_en.storyboard */; };
|
||||
5DD41D4F23DCB05600FD4AB0 /* CentralController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DD41D4E23DCB05600FD4AB0 /* CentralController.swift */; };
|
||||
5DD41D5323DD4CA400FD4AB0 /* PeripheralController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DD41D5223DD4CA400FD4AB0 /* PeripheralController.swift */; };
|
||||
7F19B4DD23F565850071A11E /* PogoInstructionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F19B4DC23F565850071A11E /* PogoInstructionsViewController.swift */; };
|
||||
7F2F0BA223EFFF75006D7404 /* OnboardingStep2ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2F0BA123EFFF75006D7404 /* OnboardingStep2ViewController.swift */; };
|
||||
7F2F0BA223EFFF75006D7404 /* AppSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2F0BA123EFFF75006D7404 /* AppSettingsViewController.swift */; };
|
||||
7F36305F23F7F81400CC6E1D /* PushNotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F36305E23F7F81400CC6E1D /* PushNotificationConstants.swift */; };
|
||||
7FACD53A23F25A9A0042A33A /* InitialScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FACD53923F25A9A0042A33A /* InitialScreenViewController.swift */; };
|
||||
7FEC361523F16A1E00127AFB /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FEC361423F16A1E00127AFB /* UIViewExtension.swift */; };
|
||||
|
@ -307,19 +338,19 @@
|
|||
B60F8BE4242659810007A641 /* UILabelExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60F8BE3242659810007A641 /* UILabelExtension.swift */; };
|
||||
B615C5A723F8EB1700345969 /* UIProgressView + Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B615C5A623F8EB1700345969 /* UIProgressView + Extension.swift */; };
|
||||
B615C5A923FA403500345969 /* UIViewController + Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B615C5A823FA403400345969 /* UIViewController + Extension.swift */; };
|
||||
C5046D5C23EF18600046E96D /* OnboardingStep1ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5046D5B23EF18600046E96D /* OnboardingStep1ViewController.swift */; };
|
||||
C5046D5C23EF18600046E96D /* RegistrationIntroViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5046D5B23EF18600046E96D /* RegistrationIntroViewController.swift */; };
|
||||
C56CF43F23F18A15006B05B0 /* OnboardingStep4ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C56CF43E23F18A15006B05B0 /* OnboardingStep4ViewController.swift */; };
|
||||
C585C83B23EEB99B0061B7C6 /* GradientButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C585C83A23EEB99B0061B7C6 /* GradientButton.swift */; };
|
||||
C58D817723F169DB00345771 /* OnboardingStep2bViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C58D817623F169DB00345771 /* OnboardingStep2bViewController.swift */; };
|
||||
C58D817723F169DB00345771 /* RegistrationSuccessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C58D817623F169DB00345771 /* RegistrationSuccessViewController.swift */; };
|
||||
C5D209FC23F4476F007233BE /* UITextViewFixed.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5D209FB23F4476F007233BE /* UITextViewFixed.swift */; };
|
||||
D8DEB6822423AE2E00D99925 /* OnboardingStep1bViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DEB6812423AE2E00D99925 /* OnboardingStep1bViewController.swift */; };
|
||||
D8DEB6822423AE2E00D99925 /* HowItWorksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DEB6812423AE2E00D99925 /* HowItWorksViewController.swift */; };
|
||||
D8EB201B23FA722D001C60EC /* HelpNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8EB201A23FA722D001C60EC /* HelpNavController.swift */; };
|
||||
D8EB201D23FBE216001C60EC /* help_center_article_style.css in Resources */ = {isa = PBXBuildFile; fileRef = D8EB201C23FBE216001C60EC /* help_center_article_style.css */; };
|
||||
F4992D5F7DAE4B13FF4BB47D /* Pods_CovidSafe_staging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FFB86126400FA51506E1B416 /* Pods_CovidSafe_staging.framework */; };
|
||||
FB12C4C1242F0480007E893B /* RespondToAuthChallengeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB12C4C0242F047F007E893B /* RespondToAuthChallengeAPI.swift */; };
|
||||
FB12C4C3242F0FE9007E893B /* GetTempIdAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB12C4C2242F0FE9007E893B /* GetTempIdAPI.swift */; };
|
||||
FB12C4C524304AF0007E893B /* OnboardingStep1aViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB12C4C424304AF0007E893B /* OnboardingStep1aViewController.swift */; };
|
||||
FBBBFCE82430A933002B174D /* OnboardingStep1aViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB12C4C424304AF0007E893B /* OnboardingStep1aViewController.swift */; };
|
||||
FB12C4C524304AF0007E893B /* PrivacyPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB12C4C424304AF0007E893B /* PrivacyPolicyViewController.swift */; };
|
||||
FBBBFCE82430A933002B174D /* PrivacyPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB12C4C424304AF0007E893B /* PrivacyPolicyViewController.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
|
@ -353,6 +384,23 @@
|
|||
46A5730925DA6B664DFE9546 /* Pods-CovidSafe-staging.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CovidSafe-staging.debug.xcconfig"; path = "Target Support Files/Pods-CovidSafe-staging/Pods-CovidSafe-staging.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
49A22E09D113DF058C94C6E6 /* Pods-CovidSafe-staging.covid-staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CovidSafe-staging.covid-staging.xcconfig"; path = "Target Support Files/Pods-CovidSafe-staging/Pods-CovidSafe-staging.covid-staging.xcconfig"; sourceTree = "<group>"; };
|
||||
5904A5C12462471A008C8012 /* EncounterV1toV2.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = EncounterV1toV2.xcmappingmodel; sourceTree = "<group>"; };
|
||||
590545EE2543E0F5009B82AD /* BLESensor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BLESensor.swift; sourceTree = "<group>"; };
|
||||
590545EF2543E0F5009B82AD /* BLEDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BLEDatabase.swift; sourceTree = "<group>"; };
|
||||
590545F02543E0F5009B82AD /* BLEUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BLEUtilities.swift; sourceTree = "<group>"; };
|
||||
590545F12543E0F5009B82AD /* BLETransmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BLETransmitter.swift; sourceTree = "<group>"; };
|
||||
590545F22543E0F5009B82AD /* BLEReceiver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BLEReceiver.swift; sourceTree = "<group>"; };
|
||||
590545F42543E0F5009B82AD /* AwakeSensor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AwakeSensor.swift; sourceTree = "<group>"; };
|
||||
590545F52543E0F5009B82AD /* Sensor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sensor.swift; sourceTree = "<group>"; };
|
||||
590546012543E0F5009B82AD /* SensorArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SensorArray.swift; sourceTree = "<group>"; };
|
||||
590546022543E0F5009B82AD /* PayloadDataSupplier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PayloadDataSupplier.swift; sourceTree = "<group>"; };
|
||||
590546042543E0F5009B82AD /* TextFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFile.swift; sourceTree = "<group>"; };
|
||||
590546052543E0F5009B82AD /* BatteryLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryLog.swift; sourceTree = "<group>"; };
|
||||
590546062543E0F5009B82AD /* DetectionLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetectionLog.swift; sourceTree = "<group>"; };
|
||||
590546072543E0F5009B82AD /* SensorLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SensorLogger.swift; sourceTree = "<group>"; };
|
||||
590546082543E0F5009B82AD /* ContactLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactLog.swift; sourceTree = "<group>"; };
|
||||
590546092543E0F5009B82AD /* StatisticsLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatisticsLog.swift; sourceTree = "<group>"; };
|
||||
5905460A2543E0F5009B82AD /* SensorDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SensorDelegate.swift; sourceTree = "<group>"; };
|
||||
5905460B2543E0F5009B82AD /* herald.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = herald.h; sourceTree = "<group>"; };
|
||||
5909E4AA245043C400D41C26 /* CovidPersistentContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CovidPersistentContainer.swift; sourceTree = "<group>"; };
|
||||
592CBB7F2441A583001FFCE9 /* PersonalDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalDetailsViewController.swift; sourceTree = "<group>"; };
|
||||
59490B61245FE22C00C9802B /* ModelV2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = ModelV2.xcdatamodel; sourceTree = "<group>"; };
|
||||
|
@ -432,6 +480,9 @@
|
|||
5BA33A7D24B55E7D00D12515 /* BLELogRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BLELogRecord.swift; sourceTree = "<group>"; };
|
||||
5BA33A7F24B55EF200D12515 /* BLELogDB.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BLELogDB.swift; sourceTree = "<group>"; };
|
||||
5BA33A8124B5602500D12515 /* BLELogRecord+BLELogSave.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BLELogRecord+BLELogSave.swift"; sourceTree = "<group>"; };
|
||||
5BBC571C25526F99005E90AA /* staging-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "staging-Info.plist"; sourceTree = "<group>"; };
|
||||
5BBE61B025633B6D00B8C983 /* CSGenericContentView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CSGenericContentView.xib; sourceTree = "<group>"; };
|
||||
5BBE61B625633E8D00B8C983 /* CSGenericViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSGenericViewController.swift; sourceTree = "<group>"; };
|
||||
5BD3EE8224330E1A0004A007 /* UploadDataStep2VC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadDataStep2VC.swift; sourceTree = "<group>"; };
|
||||
5BED1E2F24A95ECB0066C4D2 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5BED1E3024A95ECB0066C4D2 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
|
@ -460,17 +511,14 @@
|
|||
5D8DD05F23E319B300E097EF /* Encounter+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encounter+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
5D8DD06023E319B300E097EF /* Encounter+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encounter+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
5DC7B8D0242B8536008E1715 /* BluetraceConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetraceConfig.swift; sourceTree = "<group>"; };
|
||||
5DC7B8D2242B8D87008E1715 /* BluetraceUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetraceUtils.swift; sourceTree = "<group>"; };
|
||||
5DD41D3723DCB03B00FD4AB0 /* COVIDSafe.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = COVIDSafe.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5DD41D3A23DCB03B00FD4AB0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
5DD41D4323DCB03D00FD4AB0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
5DD41D4823DCB03D00FD4AB0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
5DD41D4E23DCB05600FD4AB0 /* CentralController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CentralController.swift; sourceTree = "<group>"; };
|
||||
5DD41D5223DD4CA400FD4AB0 /* PeripheralController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralController.swift; sourceTree = "<group>"; };
|
||||
5DD41D7823DE141700FD4AB0 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = "<group>"; };
|
||||
5DDB85EA23EE39C000B186BC /* Project Bluetrace.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Project Bluetrace.entitlements"; sourceTree = "<group>"; };
|
||||
7F19B4DC23F565850071A11E /* PogoInstructionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PogoInstructionsViewController.swift; sourceTree = "<group>"; };
|
||||
7F2F0BA123EFFF75006D7404 /* OnboardingStep2ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStep2ViewController.swift; sourceTree = "<group>"; };
|
||||
7F2F0BA123EFFF75006D7404 /* AppSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsViewController.swift; sourceTree = "<group>"; };
|
||||
7F36305E23F7F81400CC6E1D /* PushNotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationConstants.swift; sourceTree = "<group>"; };
|
||||
7FACD53923F25A9A0042A33A /* InitialScreenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialScreenViewController.swift; sourceTree = "<group>"; };
|
||||
7FEC361423F16A1E00127AFB /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = "<group>"; };
|
||||
|
@ -506,18 +554,18 @@
|
|||
B60F8BE3242659810007A641 /* UILabelExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UILabelExtension.swift; sourceTree = "<group>"; };
|
||||
B615C5A623F8EB1700345969 /* UIProgressView + Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIProgressView + Extension.swift"; sourceTree = "<group>"; };
|
||||
B615C5A823FA403400345969 /* UIViewController + Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController + Extension.swift"; sourceTree = "<group>"; };
|
||||
C5046D5B23EF18600046E96D /* OnboardingStep1ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStep1ViewController.swift; sourceTree = "<group>"; };
|
||||
C5046D5B23EF18600046E96D /* RegistrationIntroViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationIntroViewController.swift; sourceTree = "<group>"; };
|
||||
C56CF43E23F18A15006B05B0 /* OnboardingStep4ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStep4ViewController.swift; sourceTree = "<group>"; };
|
||||
C585C83A23EEB99B0061B7C6 /* GradientButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButton.swift; sourceTree = "<group>"; };
|
||||
C58D817623F169DB00345771 /* OnboardingStep2bViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStep2bViewController.swift; sourceTree = "<group>"; };
|
||||
C58D817623F169DB00345771 /* RegistrationSuccessViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationSuccessViewController.swift; sourceTree = "<group>"; };
|
||||
C5D209FB23F4476F007233BE /* UITextViewFixed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextViewFixed.swift; sourceTree = "<group>"; };
|
||||
D8DEB6812423AE2E00D99925 /* OnboardingStep1bViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStep1bViewController.swift; sourceTree = "<group>"; };
|
||||
D8DEB6812423AE2E00D99925 /* HowItWorksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowItWorksViewController.swift; sourceTree = "<group>"; };
|
||||
D8EB201A23FA722D001C60EC /* HelpNavController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpNavController.swift; sourceTree = "<group>"; };
|
||||
D8EB201C23FBE216001C60EC /* help_center_article_style.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = help_center_article_style.css; sourceTree = "<group>"; };
|
||||
DC24373E23F51531007BDBDF /* covid-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "covid-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
FB12C4C0242F047F007E893B /* RespondToAuthChallengeAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RespondToAuthChallengeAPI.swift; sourceTree = "<group>"; };
|
||||
FB12C4C2242F0FE9007E893B /* GetTempIdAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetTempIdAPI.swift; sourceTree = "<group>"; };
|
||||
FB12C4C424304AF0007E893B /* OnboardingStep1aViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingStep1aViewController.swift; sourceTree = "<group>"; };
|
||||
FB12C4C424304AF0007E893B /* PrivacyPolicyViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyViewController.swift; sourceTree = "<group>"; };
|
||||
FB81BAD81D73C6FD42202A04 /* Pods-CovidSafe-staging.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CovidSafe-staging.release.xcconfig"; path = "Target Support Files/Pods-CovidSafe-staging/Pods-CovidSafe-staging.release.xcconfig"; sourceTree = "<group>"; };
|
||||
FFB86126400FA51506E1B416 /* Pods_CovidSafe_staging.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CovidSafe_staging.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
@ -586,34 +634,22 @@
|
|||
30E91BEC23EFEA14002D592A /* View Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5BBE61D625637BB100B8C983 /* Debug */,
|
||||
5BBE61D525637B7600B8C983 /* Registration */,
|
||||
1B86118F24303ED400EA4B6B /* Questions */,
|
||||
5B728B4624B5667000654ABC /* BLELogViewController.swift */,
|
||||
5D8DD05923E2F08400E097EF /* ContactViewController.swift */,
|
||||
5B92B0BF24F4DC3A0069C57D /* CovidStatisticsViewController.swift */,
|
||||
5B9E41112521688700434B25 /* CSAlertViewController.swift */,
|
||||
5BBE61B625633E8D00B8C983 /* CSGenericViewController.swift */,
|
||||
D8EB201A23FA722D001C60EC /* HelpNavController.swift */,
|
||||
5D5F83AF23F045A800770DEF /* HomeViewController.swift */,
|
||||
5B92B0AD24EFADB00069C57D /* SettingsViewController.swift */,
|
||||
5D8DD05D23E2F0BA00E097EF /* InfoViewController.swift */,
|
||||
7FACD53923F25A9A0042A33A /* InitialScreenViewController.swift */,
|
||||
5B92B09324D14AE00069C57D /* InternetConnectionViewController.swift */,
|
||||
5B9E41112521688700434B25 /* CSAlertViewController.swift */,
|
||||
5B7ABF24244D3BC600BB249B /* IsolationSuccessViewController.swift */,
|
||||
5D8DD05B23E2F0A700E097EF /* LogViewController.swift */,
|
||||
5961ABE92474E358004040DF /* MigrationViewController.swift */,
|
||||
FB12C4C424304AF0007E893B /* OnboardingStep1aViewController.swift */,
|
||||
D8DEB6812423AE2E00D99925 /* OnboardingStep1bViewController.swift */,
|
||||
C5046D5B23EF18600046E96D /* OnboardingStep1ViewController.swift */,
|
||||
C58D817623F169DB00345771 /* OnboardingStep2bViewController.swift */,
|
||||
7F2F0BA123EFFF75006D7404 /* OnboardingStep2ViewController.swift */,
|
||||
C56CF43E23F18A15006B05B0 /* OnboardingStep4ViewController.swift */,
|
||||
30BE1CB423F15D47005DCE4F /* OTPViewController.swift */,
|
||||
592CBB7F2441A583001FFCE9 /* PersonalDetailsViewController.swift */,
|
||||
5D8B248A23F146C500DBB74D /* PhoneNumberViewController.swift */,
|
||||
7F19B4DC23F565850071A11E /* PogoInstructionsViewController.swift */,
|
||||
59B7416F24514126006E1EEA /* RegistrationConsentViewController.swift */,
|
||||
5B110C0F248F275A00B68291 /* SelectCountryViewController.swift */,
|
||||
5B92B0AD24EFADB00069C57D /* SettingsViewController.swift */,
|
||||
B615C5A823FA403400345969 /* UIViewController + Extension.swift */,
|
||||
5B7ABF27244D6BE100BB249B /* UnderSixteenViewController.swift */,
|
||||
1B86118F24303ED400EA4B6B /* Questions */,
|
||||
);
|
||||
name = "View Controllers";
|
||||
sourceTree = "<group>";
|
||||
|
@ -622,9 +658,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
5D269C0C23E2958F00ADF2DE /* BluetraceManager.swift */,
|
||||
5DD41D4E23DCB05600FD4AB0 /* CentralController.swift */,
|
||||
5DC7B8D2242B8D87008E1715 /* BluetraceUtils.swift */,
|
||||
5DD41D5223DD4CA400FD4AB0 /* PeripheralController.swift */,
|
||||
5D5F83AC23F023F600770DEF /* EncounterMessageManager.swift */,
|
||||
5DC7B8D0242B8536008E1715 /* BluetraceConfig.swift */,
|
||||
);
|
||||
|
@ -636,11 +669,17 @@
|
|||
children = (
|
||||
5B9360EC24F6196A008859FC /* Cells */,
|
||||
30E91BEA23EFEA0B002D592A /* CodeInputView.swift */,
|
||||
5B9E410C2521683500434B25 /* CSAlertView.xib */,
|
||||
B615C5A623F8EB1700345969 /* UIProgressView + Extension.swift */,
|
||||
0BC141AB24305D9C00399FA8 /* NSMutableString + Extensions.swift */,
|
||||
0BC141AD2430685800399FA8 /* UIColor + Extensions.swift */,
|
||||
B615C5A623F8EB1700345969 /* UIProgressView + Extension.swift */,
|
||||
0B55E1912430760500C9E798 /* UITextView + Extensions.swift */,
|
||||
5B92B0BC24F4DBE20069C57D /* CovidStatisticsView.xib */,
|
||||
5B9E410C2521683500434B25 /* CSAlertView.xib */,
|
||||
5BBE61B025633B6D00B8C983 /* CSGenericContentView.xib */,
|
||||
5B92B0A424EF61480069C57D /* HomeView.xib */,
|
||||
5B92B0B324F37E3E0069C57D /* InternetConnectionView.xib */,
|
||||
5B92B0AA24EFA3160069C57D /* SettingsView.xib */,
|
||||
5DD41D4023DCB03B00FD4AB0 /* Main.storyboard */,
|
||||
);
|
||||
name = Views;
|
||||
sourceTree = "<group>";
|
||||
|
@ -660,6 +699,70 @@
|
|||
name = Utils;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
590545EB2543E0F5009B82AD /* Herald */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
590545EC2543E0F5009B82AD /* Sensor */,
|
||||
5905460B2543E0F5009B82AD /* herald.h */,
|
||||
);
|
||||
path = Herald;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
590545EC2543E0F5009B82AD /* Sensor */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
590545ED2543E0F5009B82AD /* BLE */,
|
||||
590545F32543E0F5009B82AD /* Location */,
|
||||
590545F52543E0F5009B82AD /* Sensor.swift */,
|
||||
590545F62543E0F5009B82AD /* Payload */,
|
||||
590546012543E0F5009B82AD /* SensorArray.swift */,
|
||||
590546022543E0F5009B82AD /* PayloadDataSupplier.swift */,
|
||||
590546032543E0F5009B82AD /* Data */,
|
||||
5905460A2543E0F5009B82AD /* SensorDelegate.swift */,
|
||||
);
|
||||
path = Sensor;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
590545ED2543E0F5009B82AD /* BLE */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
590545EE2543E0F5009B82AD /* BLESensor.swift */,
|
||||
590545EF2543E0F5009B82AD /* BLEDatabase.swift */,
|
||||
590545F02543E0F5009B82AD /* BLEUtilities.swift */,
|
||||
590545F12543E0F5009B82AD /* BLETransmitter.swift */,
|
||||
590545F22543E0F5009B82AD /* BLEReceiver.swift */,
|
||||
);
|
||||
path = BLE;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
590545F32543E0F5009B82AD /* Location */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
590545F42543E0F5009B82AD /* AwakeSensor.swift */,
|
||||
);
|
||||
path = Location;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
590545F62543E0F5009B82AD /* Payload */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = Payload;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
590546032543E0F5009B82AD /* Data */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
590546042543E0F5009B82AD /* TextFile.swift */,
|
||||
590546052543E0F5009B82AD /* BatteryLog.swift */,
|
||||
590546062543E0F5009B82AD /* DetectionLog.swift */,
|
||||
590546072543E0F5009B82AD /* SensorLogger.swift */,
|
||||
590546082543E0F5009B82AD /* ContactLog.swift */,
|
||||
590546092543E0F5009B82AD /* StatisticsLog.swift */,
|
||||
);
|
||||
path = Data;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5909E4A92450438A00D41C26 /* CoreData */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -767,6 +870,35 @@
|
|||
name = Cells;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5BBE61D525637B7600B8C983 /* Registration */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7F2F0BA123EFFF75006D7404 /* AppSettingsViewController.swift */,
|
||||
D8DEB6812423AE2E00D99925 /* HowItWorksViewController.swift */,
|
||||
C56CF43E23F18A15006B05B0 /* OnboardingStep4ViewController.swift */,
|
||||
30BE1CB423F15D47005DCE4F /* OTPViewController.swift */,
|
||||
592CBB7F2441A583001FFCE9 /* PersonalDetailsViewController.swift */,
|
||||
5D8B248A23F146C500DBB74D /* PhoneNumberViewController.swift */,
|
||||
FB12C4C424304AF0007E893B /* PrivacyPolicyViewController.swift */,
|
||||
59B7416F24514126006E1EEA /* RegistrationConsentViewController.swift */,
|
||||
C5046D5B23EF18600046E96D /* RegistrationIntroViewController.swift */,
|
||||
C58D817623F169DB00345771 /* RegistrationSuccessViewController.swift */,
|
||||
5B110C0F248F275A00B68291 /* SelectCountryViewController.swift */,
|
||||
5B7ABF27244D6BE100BB249B /* UnderSixteenViewController.swift */,
|
||||
);
|
||||
name = Registration;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5BBE61D625637BB100B8C983 /* Debug */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5D8DD05923E2F08400E097EF /* ContactViewController.swift */,
|
||||
5D8DD05D23E2F0BA00E097EF /* InfoViewController.swift */,
|
||||
5D8DD05B23E2F0A700E097EF /* LogViewController.swift */,
|
||||
);
|
||||
name = Debug;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5BED1E3824A96CF50066C4D2 /* LaunchScreen */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -824,6 +956,7 @@
|
|||
5DD41D3923DCB03B00FD4AB0 /* CovidSafe */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
590545EB2543E0F5009B82AD /* Herald */,
|
||||
597BB7CA245FACE20067A2E2 /* Crypto */,
|
||||
5949DC522434859600AE76BC /* lottie */,
|
||||
5B92D661243005B10049877B /* Resources */,
|
||||
|
@ -843,14 +976,10 @@
|
|||
30FADD6C23F520F5006C125F /* Utils */,
|
||||
5DD41D7723DE141700FD4AB0 /* tracer.xcdatamodeld */,
|
||||
5B728B4C24B5A26D00654ABC /* debug.xcdatamodeld */,
|
||||
5B92B0BC24F4DBE20069C57D /* CovidStatisticsView.xib */,
|
||||
5B92B0A424EF61480069C57D /* HomeView.xib */,
|
||||
5B92B0B324F37E3E0069C57D /* InternetConnectionView.xib */,
|
||||
5B92B0AA24EFA3160069C57D /* SettingsView.xib */,
|
||||
5DD41D4023DCB03B00FD4AB0 /* Main.storyboard */,
|
||||
D8EB201C23FBE216001C60EC /* help_center_article_style.css */,
|
||||
5DD41D4323DCB03D00FD4AB0 /* Assets.xcassets */,
|
||||
5DD41D4823DCB03D00FD4AB0 /* Info.plist */,
|
||||
5BBC571C25526F99005E90AA /* staging-Info.plist */,
|
||||
5D269C0A23E22CC400ADF2DE /* DeviceIdentifier.swift */,
|
||||
DC24373E23F51531007BDBDF /* covid-Bridging-Header.h */,
|
||||
);
|
||||
|
@ -1088,6 +1217,7 @@
|
|||
5BED1E3B24A96D1C0066C4D2 /* LaunchScreen_ar.storyboard in Resources */,
|
||||
5BED1E4424A98EB60066C4D2 /* LaunchScreen_vi.storyboard in Resources */,
|
||||
5B30F78D24B817A100CDED63 /* LaunchScreen_el.storyboard in Resources */,
|
||||
5BBC571D25526F99005E90AA /* staging-Info.plist in Resources */,
|
||||
5B9360FE24F73CE6008859FC /* LoadingViewCell.xib in Resources */,
|
||||
5B92D6C9243018040049877B /* LaunchScreen_en.storyboard in Resources */,
|
||||
5B92B0A624EF61490069C57D /* HomeView.xib in Resources */,
|
||||
|
@ -1096,6 +1226,7 @@
|
|||
5B92D6CB243018040049877B /* Assets.xcassets in Resources */,
|
||||
5B92B0A324E0CD190069C57D /* LaunchScreen_tr.storyboard in Resources */,
|
||||
5BED1E4124A98EAF0066C4D2 /* LaunchScreen_zh-Hant.storyboard in Resources */,
|
||||
5BBE61B225633B6D00B8C983 /* CSGenericContentView.xib in Resources */,
|
||||
5B30F78A24B8179600CDED63 /* LaunchScreen_it.storyboard in Resources */,
|
||||
5B337AB0245AA26300537620 /* Spinner_upload.json in Resources */,
|
||||
5B92B0B524F37E3E0069C57D /* InternetConnectionView.xib in Resources */,
|
||||
|
@ -1129,6 +1260,7 @@
|
|||
5BED1E3A24A96D1C0066C4D2 /* LaunchScreen_ar.storyboard in Resources */,
|
||||
5B30F78C24B817A100CDED63 /* LaunchScreen_el.storyboard in Resources */,
|
||||
5B92B0A124E0CD140069C57D /* LaunchScreen_pa-IN.storyboard in Resources */,
|
||||
5BBE61B125633B6D00B8C983 /* CSGenericContentView.xib in Resources */,
|
||||
5B82435E2480DC2100705CB1 /* Localizable.strings in Resources */,
|
||||
5B92D663243011B40049877B /* CovidSafe-config.plist in Resources */,
|
||||
5BED1E4324A98EB60066C4D2 /* LaunchScreen_vi.storyboard in Resources */,
|
||||
|
@ -1255,32 +1387,36 @@
|
|||
5B92D67D243018040049877B /* HomeViewController.swift in Sources */,
|
||||
5B92D67E243018040049877B /* InfoViewController.swift in Sources */,
|
||||
5B92D67F243018040049877B /* PogoInstructionsViewController.swift in Sources */,
|
||||
FBBBFCE82430A933002B174D /* OnboardingStep1aViewController.swift in Sources */,
|
||||
FBBBFCE82430A933002B174D /* PrivacyPolicyViewController.swift in Sources */,
|
||||
5B51ED512485DF9C008CE722 /* UILocalization.swift in Sources */,
|
||||
5BA33A8024B55EF200D12515 /* BLELogDB.swift in Sources */,
|
||||
594E77C3247387B1009B8B34 /* EncounterDB+migration.swift in Sources */,
|
||||
590546292543E0F6009B82AD /* SensorArray.swift in Sources */,
|
||||
590C99332432C1C400A5EC71 /* UploadDataHomeViewController.swift in Sources */,
|
||||
590546112543E0F5009B82AD /* BLEUtilities.swift in Sources */,
|
||||
59B7417124514126006E1EEA /* RegistrationConsentViewController.swift in Sources */,
|
||||
5B92B0AF24EFADB00069C57D /* SettingsViewController.swift in Sources */,
|
||||
5B92B0C124F4DC3A0069C57D /* CovidStatisticsViewController.swift in Sources */,
|
||||
5B92D680243018040049877B /* CodeInputView.swift in Sources */,
|
||||
5B92D681243018040049877B /* EncounterRecord.swift in Sources */,
|
||||
5905462D2543E0F6009B82AD /* TextFile.swift in Sources */,
|
||||
5B728B4B24B581C100654ABC /* BLELog+CoreDataProperties.swift in Sources */,
|
||||
5B337AAD245A9EF800537620 /* UploadDataErrorViewController.swift in Sources */,
|
||||
590888B02431B9E7008C9B9F /* UIColor + Extensions.swift in Sources */,
|
||||
5B728B4E24B5A26D00654ABC /* debug.xcdatamodeld in Sources */,
|
||||
5905460F2543E0F5009B82AD /* BLEDatabase.swift in Sources */,
|
||||
5B92D74F243022EF0049877B /* DataUploadS3.swift in Sources */,
|
||||
5B92D683243018040049877B /* OnboardingStep2bViewController.swift in Sources */,
|
||||
5B92D683243018040049877B /* RegistrationSuccessViewController.swift in Sources */,
|
||||
5BA33A8224B5602500D12515 /* BLELogRecord+BLELogSave.swift in Sources */,
|
||||
5B92B09524D14AE00069C57D /* InternetConnectionViewController.swift in Sources */,
|
||||
5B92D684243018040049877B /* AsyncAction.swift in Sources */,
|
||||
5B92D685243018040049877B /* UILabelExtension.swift in Sources */,
|
||||
5905462B2543E0F6009B82AD /* PayloadDataSupplier.swift in Sources */,
|
||||
5B92D686243018040049877B /* AppDelegate.swift in Sources */,
|
||||
5B92D687243018040049877B /* PhoneNumberViewController.swift in Sources */,
|
||||
5BA33A7C24B41ECA00D12515 /* UIFont + Traits.swift in Sources */,
|
||||
5961ABEB2474E358004040DF /* MigrationViewController.swift in Sources */,
|
||||
5B110C11248F275B00B68291 /* SelectCountryViewController.swift in Sources */,
|
||||
5B92D688243018040049877B /* BluetraceUtils.swift in Sources */,
|
||||
5B92D689243018040049877B /* NewFeedbackFlowController.swift in Sources */,
|
||||
5B92D68A243018040049877B /* Outcome.swift in Sources */,
|
||||
5B92D68B243018040049877B /* Encounter+EncounterRecord.swift in Sources */,
|
||||
|
@ -1302,12 +1438,13 @@
|
|||
597BB7CD245FAEC00067A2E2 /* SecKey+CovidSafe.swift in Sources */,
|
||||
597BB7D0245FB1250067A2E2 /* Crypto.swift in Sources */,
|
||||
5B92D693243018040049877B /* UIColor+Hex.swift in Sources */,
|
||||
5B92D694243018040049877B /* OnboardingStep1ViewController.swift in Sources */,
|
||||
590546372543E0F6009B82AD /* StatisticsLog.swift in Sources */,
|
||||
5B92D694243018040049877B /* RegistrationIntroViewController.swift in Sources */,
|
||||
5B92D695243018040049877B /* DeviceInfoExtension.swift in Sources */,
|
||||
5BD3EE84243313450004A007 /* UploadDataStep2VC.swift in Sources */,
|
||||
5B92D697243018040049877B /* PeripheralController.swift in Sources */,
|
||||
5B92D698243018040049877B /* SendFeedbackAction.swift in Sources */,
|
||||
5B92D699243018040049877B /* UITextViewFixed.swift in Sources */,
|
||||
590546152543E0F6009B82AD /* BLEReceiver.swift in Sources */,
|
||||
5BA33A7E24B55E7D00D12515 /* BLELogRecord.swift in Sources */,
|
||||
590888B12431B9EB008C9B9F /* Question3ViewController.swift in Sources */,
|
||||
5957EB67244E936E002F5388 /* UnderSixteenViewController.swift in Sources */,
|
||||
|
@ -1323,6 +1460,7 @@
|
|||
5B92D6A0243018040049877B /* CountriesData.swift in Sources */,
|
||||
590888B22431B9EF008C9B9F /* Question2ViewController.swift in Sources */,
|
||||
0B42D0E02432B3AF00E4F44C /* QuestionUploadDataViewController.swift in Sources */,
|
||||
590546352543E0F6009B82AD /* ContactLog.swift in Sources */,
|
||||
5B92D6A1243018040049877B /* AlertController.swift in Sources */,
|
||||
5B92D750243022F20049877B /* InitiateUploadAPI.swift in Sources */,
|
||||
5B92D69F243018040049877B /* EncounterMessageManager.swift in Sources */,
|
||||
|
@ -1330,29 +1468,37 @@
|
|||
5B728B4924B5816C00654ABC /* BLELog+CoreDataClass.swift in Sources */,
|
||||
5B92D6A2243018040049877B /* GetTempIdAPI.swift in Sources */,
|
||||
5B92D6A3243018040049877B /* PhoneValidationAPI.swift in Sources */,
|
||||
590546132543E0F6009B82AD /* BLETransmitter.swift in Sources */,
|
||||
59AF2EB32435A38100ACCAF2 /* CovidRequestRetrier.swift in Sources */,
|
||||
592CBB812441A583001FFCE9 /* PersonalDetailsViewController.swift in Sources */,
|
||||
5B92D6A4243018040049877B /* CentralController.swift in Sources */,
|
||||
5B92D6A5243018040049877B /* UIWindow+TopMost.swift in Sources */,
|
||||
5B92D6A6243018040049877B /* OnboardingStep1bViewController.swift in Sources */,
|
||||
5B92D6A6243018040049877B /* HowItWorksViewController.swift in Sources */,
|
||||
590546312543E0F6009B82AD /* DetectionLog.swift in Sources */,
|
||||
5B92D6A7243018040049877B /* OnboardingStep4ViewController.swift in Sources */,
|
||||
5B92D6A8243018040049877B /* PresentFeedbackExtensions.swift in Sources */,
|
||||
590546392543E0F6009B82AD /* SensorDelegate.swift in Sources */,
|
||||
5B92D6A9243018040049877B /* Errors.swift in Sources */,
|
||||
5B92D6AA243018040049877B /* UINavigationBar+Style.swift in Sources */,
|
||||
590546172543E0F6009B82AD /* AwakeSensor.swift in Sources */,
|
||||
5B92D6AB243018040049877B /* UploadFileData.swift in Sources */,
|
||||
590888B52431BA76008C9B9F /* NSMutableString + Extensions.swift in Sources */,
|
||||
5B92D6AC243018040049877B /* BundleInfoExtension.swift in Sources */,
|
||||
5905460D2543E0F5009B82AD /* BLESensor.swift in Sources */,
|
||||
5BBE61B825633E8D00B8C983 /* CSGenericViewController.swift in Sources */,
|
||||
5909E4AC245043C400D41C26 /* CovidPersistentContainer.swift in Sources */,
|
||||
5905462F2543E0F6009B82AD /* BatteryLog.swift in Sources */,
|
||||
5B92D6AD243018040049877B /* FeedbackSettings.swift in Sources */,
|
||||
5B92D6AE243018040049877B /* JMCTargetJSONFromDisk.swift in Sources */,
|
||||
5B92D6B0243018040049877B /* Action.swift in Sources */,
|
||||
5B92D6B1243018040049877B /* OnboardingStep2ViewController.swift in Sources */,
|
||||
5B92D6B1243018040049877B /* AppSettingsViewController.swift in Sources */,
|
||||
59AF2E9A2435533A00ACCAF2 /* CovidCertificates.swift in Sources */,
|
||||
5B92D6B2243018040049877B /* HTTPPostFeedbackAction.swift in Sources */,
|
||||
5B92B0C424F4DE6F0069C57D /* StatisticsAPI.swift in Sources */,
|
||||
590546192543E0F6009B82AD /* Sensor.swift in Sources */,
|
||||
59898604245173C200966E61 /* URLHelper.swift in Sources */,
|
||||
590888B32431B9F2008C9B9F /* Question1ViewController.swift in Sources */,
|
||||
590888B62431BA7C008C9B9F /* Question3ErrorViewController.swift in Sources */,
|
||||
590546332543E0F6009B82AD /* SensorLogger.swift in Sources */,
|
||||
5B900FC22485C4EE00CAA419 /* String+Localization.swift in Sources */,
|
||||
59AF2E9D2435581600ACCAF2 /* CovidNetworking.swift in Sources */,
|
||||
596B189D24499591003E190F /* UploadHelper.swift in Sources */,
|
||||
|
@ -1373,9 +1519,11 @@
|
|||
30E91BE923EFE514002D592A /* UploadDataVC.swift in Sources */,
|
||||
0BC141AE2430685800399FA8 /* UIColor + Extensions.swift in Sources */,
|
||||
7FEC361523F16A1E00127AFB /* UIViewExtension.swift in Sources */,
|
||||
590546122543E0F6009B82AD /* BLETransmitter.swift in Sources */,
|
||||
5D269C0B23E22CC400ADF2DE /* DeviceIdentifier.swift in Sources */,
|
||||
A767D31E242DF1B000DC9E2A /* LanguageInfoExtension.swift in Sources */,
|
||||
B605A7B12427429D008BA819 /* PlistHelper.swift in Sources */,
|
||||
5905460E2543E0F5009B82AD /* BLEDatabase.swift in Sources */,
|
||||
5DC7B8D1242B8536008E1715 /* BluetraceConfig.swift in Sources */,
|
||||
0B69E7E92430C22E00561DD9 /* UploadDataHomeViewController.swift in Sources */,
|
||||
1B86119524303F5E00EA4B6B /* Question3ViewController.swift in Sources */,
|
||||
|
@ -1389,20 +1537,23 @@
|
|||
59B7417024514126006E1EEA /* RegistrationConsentViewController.swift in Sources */,
|
||||
5D5F83B023F045A800770DEF /* HomeViewController.swift in Sources */,
|
||||
5B337AAB245A9BBC00537620 /* UploadDataErrorViewController.swift in Sources */,
|
||||
FB12C4C524304AF0007E893B /* OnboardingStep1aViewController.swift in Sources */,
|
||||
FB12C4C524304AF0007E893B /* PrivacyPolicyViewController.swift in Sources */,
|
||||
7F19B4DD23F565850071A11E /* PogoInstructionsViewController.swift in Sources */,
|
||||
30E91BEB23EFEA0B002D592A /* CodeInputView.swift in Sources */,
|
||||
597BB7CF245FB1250067A2E2 /* Crypto.swift in Sources */,
|
||||
30BE1CAF23F1349F005DCE4F /* EncounterRecord.swift in Sources */,
|
||||
590546102543E0F5009B82AD /* BLEUtilities.swift in Sources */,
|
||||
590546382543E0F6009B82AD /* SensorDelegate.swift in Sources */,
|
||||
59490B64245FE3DA00C9802B /* EncounterV2Mapping.swift in Sources */,
|
||||
C58D817723F169DB00345771 /* OnboardingStep2bViewController.swift in Sources */,
|
||||
590546282543E0F6009B82AD /* SensorArray.swift in Sources */,
|
||||
C58D817723F169DB00345771 /* RegistrationSuccessViewController.swift in Sources */,
|
||||
59ACB574242F195A00E63E3C /* InitiateUploadAPI.swift in Sources */,
|
||||
A767D324242DF1B100DC9E2A /* AsyncAction.swift in Sources */,
|
||||
B60F8BE4242659810007A641 /* UILabelExtension.swift in Sources */,
|
||||
5DD41D3B23DCB03B00FD4AB0 /* AppDelegate.swift in Sources */,
|
||||
5D8B248B23F146C500DBB74D /* PhoneNumberViewController.swift in Sources */,
|
||||
5DC7B8D3242B8D87008E1715 /* BluetraceUtils.swift in Sources */,
|
||||
A767D32D242DF1B100DC9E2A /* NewFeedbackFlowController.swift in Sources */,
|
||||
590546362543E0F6009B82AD /* StatisticsLog.swift in Sources */,
|
||||
A767D319242DF1B000DC9E2A /* Outcome.swift in Sources */,
|
||||
30BE1CB123F134D0005DCE4F /* Encounter+EncounterRecord.swift in Sources */,
|
||||
A767D336242DF1B100DC9E2A /* GetJMCTargetAction.swift in Sources */,
|
||||
|
@ -1411,25 +1562,31 @@
|
|||
D8EB201B23FA722D001C60EC /* HelpNavController.swift in Sources */,
|
||||
596B189924496D32003E190F /* Encounter+Util.swift in Sources */,
|
||||
5961ABEA2474E358004040DF /* MigrationViewController.swift in Sources */,
|
||||
590546182543E0F6009B82AD /* Sensor.swift in Sources */,
|
||||
590546162543E0F6009B82AD /* AwakeSensor.swift in Sources */,
|
||||
5B04074124BC0CEA00FAAFD0 /* MessageAPI.swift in Sources */,
|
||||
30BE1CB523F15D47005DCE4F /* OTPViewController.swift in Sources */,
|
||||
5905460C2543E0F5009B82AD /* BLESensor.swift in Sources */,
|
||||
5B900FC12485C4EE00CAA419 /* String+Localization.swift in Sources */,
|
||||
5D8DD06123E319B300E097EF /* Encounter+CoreDataClass.swift in Sources */,
|
||||
594E77BF24736B77009B8B34 /* EncounterDB.swift in Sources */,
|
||||
590546322543E0F6009B82AD /* SensorLogger.swift in Sources */,
|
||||
5905462E2543E0F6009B82AD /* BatteryLog.swift in Sources */,
|
||||
FB12C4C1242F0480007E893B /* RespondToAuthChallengeAPI.swift in Sources */,
|
||||
5D269C0D23E2958F00ADF2DE /* BluetraceManager.swift in Sources */,
|
||||
1B86119124303EF200EA4B6B /* Question1ViewController.swift in Sources */,
|
||||
5BD3EE8324330E1A0004A007 /* UploadDataStep2VC.swift in Sources */,
|
||||
A767D31B242DF1B000DC9E2A /* UIColor+Hex.swift in Sources */,
|
||||
5BA33A7B24B3FE2700D12515 /* UIFont + Traits.swift in Sources */,
|
||||
C5046D5C23EF18600046E96D /* OnboardingStep1ViewController.swift in Sources */,
|
||||
590546142543E0F6009B82AD /* BLEReceiver.swift in Sources */,
|
||||
C5046D5C23EF18600046E96D /* RegistrationIntroViewController.swift in Sources */,
|
||||
0B69E7ED2430D72900561DD9 /* UploadDataNavigationController.swift in Sources */,
|
||||
A767D320242DF1B100DC9E2A /* DeviceInfoExtension.swift in Sources */,
|
||||
5DD41D5323DD4CA400FD4AB0 /* PeripheralController.swift in Sources */,
|
||||
5B7ABF28244D6BE100BB249B /* UnderSixteenViewController.swift in Sources */,
|
||||
A767D325242DF1B100DC9E2A /* SendFeedbackAction.swift in Sources */,
|
||||
C5D209FC23F4476F007233BE /* UITextViewFixed.swift in Sources */,
|
||||
C585C83B23EEB99B0061B7C6 /* GradientButton.swift in Sources */,
|
||||
590546342543E0F6009B82AD /* ContactLog.swift in Sources */,
|
||||
597BB7CC245FAEC00067A2E2 /* SecKey+CovidSafe.swift in Sources */,
|
||||
5B92B0AE24EFADB00069C57D /* SettingsViewController.swift in Sources */,
|
||||
5B7ABF25244D3BC600BB249B /* IsolationSuccessViewController.swift in Sources */,
|
||||
|
@ -1438,6 +1595,7 @@
|
|||
5B577815245A584C0088F111 /* UploadDataPrefaceViewController.swift in Sources */,
|
||||
0BC141AC24305D9C00399FA8 /* NSMutableString + Extensions.swift in Sources */,
|
||||
59ACB576242F404500E63E3C /* DataUploadS3.swift in Sources */,
|
||||
590546302543E0F6009B82AD /* DetectionLog.swift in Sources */,
|
||||
5B92B0C024F4DC3A0069C57D /* CovidStatisticsViewController.swift in Sources */,
|
||||
A767D326242DF1B100DC9E2A /* JMCTarget.swift in Sources */,
|
||||
5D5F83AD23F023F600770DEF /* EncounterMessageManager.swift in Sources */,
|
||||
|
@ -1450,13 +1608,14 @@
|
|||
5BFFD94B242EC120003AEF4F /* PhoneValidationAPI.swift in Sources */,
|
||||
59AF2EB22435A38100ACCAF2 /* CovidRequestRetrier.swift in Sources */,
|
||||
592CBB802441A583001FFCE9 /* PersonalDetailsViewController.swift in Sources */,
|
||||
5DD41D4F23DCB05600FD4AB0 /* CentralController.swift in Sources */,
|
||||
A767D330242DF1B100DC9E2A /* UIWindow+TopMost.swift in Sources */,
|
||||
D8DEB6822423AE2E00D99925 /* OnboardingStep1bViewController.swift in Sources */,
|
||||
D8DEB6822423AE2E00D99925 /* HowItWorksViewController.swift in Sources */,
|
||||
5B92B0C324F4DE6F0069C57D /* StatisticsAPI.swift in Sources */,
|
||||
5905462C2543E0F6009B82AD /* TextFile.swift in Sources */,
|
||||
C56CF43F23F18A15006B05B0 /* OnboardingStep4ViewController.swift in Sources */,
|
||||
A767D328242DF1B100DC9E2A /* PresentFeedbackExtensions.swift in Sources */,
|
||||
A767D32F242DF1B100DC9E2A /* Errors.swift in Sources */,
|
||||
5905462A2543E0F6009B82AD /* PayloadDataSupplier.swift in Sources */,
|
||||
0B22A56B242F286900D1FE60 /* UINavigationBar+Style.swift in Sources */,
|
||||
0B1810122431EE610005D11F /* PhoneNumberParser.swift in Sources */,
|
||||
1B86119B24303FA200EA4B6B /* Question3ErrorViewController.swift in Sources */,
|
||||
|
@ -1465,10 +1624,11 @@
|
|||
A767D31D242DF1B000DC9E2A /* BundleInfoExtension.swift in Sources */,
|
||||
A767D32C242DF1B100DC9E2A /* FeedbackSettings.swift in Sources */,
|
||||
A767D31A242DF1B000DC9E2A /* JMCTargetJSONFromDisk.swift in Sources */,
|
||||
5BBE61B725633E8D00B8C983 /* CSGenericViewController.swift in Sources */,
|
||||
5B92B09424D14AE00069C57D /* InternetConnectionViewController.swift in Sources */,
|
||||
A767D334242DF1B100DC9E2A /* Action.swift in Sources */,
|
||||
59AF2E992435533A00ACCAF2 /* CovidCertificates.swift in Sources */,
|
||||
7F2F0BA223EFFF75006D7404 /* OnboardingStep2ViewController.swift in Sources */,
|
||||
7F2F0BA223EFFF75006D7404 /* AppSettingsViewController.swift in Sources */,
|
||||
594E77C2247387B1009B8B34 /* EncounterDB+migration.swift in Sources */,
|
||||
59898603245173C200966E61 /* URLHelper.swift in Sources */,
|
||||
A767D31F242DF1B000DC9E2A /* HTTPPostFeedbackAction.swift in Sources */,
|
||||
|
@ -1627,7 +1787,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
DEVELOPMENT_TEAM = 45792XH5L8;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
|
@ -1635,7 +1795,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.14;
|
||||
MARKETING_VERSION = 2.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe;
|
||||
PRODUCT_NAME = COVIDSafe;
|
||||
|
@ -1711,7 +1871,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
DEVELOPMENT_TEAM = 45792XH5L8;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
|
@ -1719,7 +1879,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.14;
|
||||
MARKETING_VERSION = 2.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe;
|
||||
PRODUCT_NAME = COVIDSafe;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -1739,15 +1899,15 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
DEVELOPMENT_TEAM = 45792XH5L8;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/Info.plist";
|
||||
INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/staging-Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.14;
|
||||
MARKETING_VERSION = 2.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS -D DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe;
|
||||
|
@ -1769,15 +1929,15 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
DEVELOPMENT_TEAM = 45792XH5L8;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/Info.plist";
|
||||
INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/staging-Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.14;
|
||||
MARKETING_VERSION = 2.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS -D DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe;
|
||||
|
@ -1799,15 +1959,15 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
DEVELOPMENT_TEAM = 45792XH5L8;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/Info.plist";
|
||||
INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/staging-Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.14;
|
||||
MARKETING_VERSION = 2.0;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS -D DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe;
|
||||
PRODUCT_MODULE_NAME = COVIDSafe;
|
||||
|
@ -1829,15 +1989,15 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
DEVELOPMENT_TEAM = 45792XH5L8;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/Info.plist";
|
||||
INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/staging-Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.14;
|
||||
MARKETING_VERSION = 2.0;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS -D DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe;
|
||||
PRODUCT_MODULE_NAME = COVIDSafe;
|
||||
|
@ -1977,7 +2137,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
DEVELOPMENT_TEAM = 45792XH5L8;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
|
@ -1985,7 +2145,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.14;
|
||||
MARKETING_VERSION = 2.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe;
|
||||
PRODUCT_NAME = COVIDSafe;
|
||||
|
@ -2005,7 +2165,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
DEVELOPMENT_TEAM = 45792XH5L8;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
|
@ -2013,7 +2173,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.14;
|
||||
MARKETING_VERSION = 2.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe;
|
||||
PRODUCT_NAME = COVIDSafe;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
|
@ -29,14 +29,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
UIBarButtonItem.appearance().setTitleTextAttributes([.foregroundColor: UIColor.covidSafeColor], for: .normal)
|
||||
UINavigationBar.appearance().tintColor = UIColor.covidSafeColor
|
||||
|
||||
let hasUserConsent = true
|
||||
let hasUserCompletedOnboarding = UserDefaults.standard.bool(forKey: "turnedOnBluetooth")
|
||||
let bluetoothAuthorised = BluetraceManager.shared.isBluetoothAuthorized()
|
||||
if (hasUserConsent && hasUserCompletedOnboarding && bluetoothAuthorised) {
|
||||
BluetraceManager.shared.turnOn()
|
||||
} else {
|
||||
print("Onboarding not yet done.")
|
||||
}
|
||||
EncounterMessageManager.shared.setup()
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
|
||||
|
|
63
CovidSafe/AppSettingsViewController.swift
Normal file
63
CovidSafe/AppSettingsViewController.swift
Normal file
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// AppSettingsViewController.swift
|
||||
// CovidSafe
|
||||
//
|
||||
// Copyright © 2020 Australian Government. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreBluetooth
|
||||
import UserNotifications
|
||||
|
||||
class AppSettingsViewController: UIViewController {
|
||||
private var backupSensorDidUpdateStateCallback: ((SensorState, SensorType?) -> Void)?
|
||||
|
||||
@IBOutlet weak var stepCounterLabel: UILabel!
|
||||
@IBOutlet weak var topContentTextView: UITextView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
stepCounterLabel.text = String.localizedStringWithFormat( "stepCounter".localizedString(),
|
||||
4,
|
||||
4
|
||||
)
|
||||
topContentTextView.addLink("\(URLHelper.getHelpURL())#location-permission-android", enclosedIn: "*")
|
||||
topContentTextView.addAllBold(enclosedIn: "#")
|
||||
}
|
||||
|
||||
@IBAction func proceedTapped(_ sender: UIButton) {
|
||||
self.backupSensorDidUpdateStateCallback = BluetraceManager.shared.sensorDidUpdateStateCallback
|
||||
BluetraceManager.shared.sensorDidUpdateStateCallback = sensorManagerDidUpdateBluetoothCallback
|
||||
BluetraceManager.shared.turnOnBLE()
|
||||
UserDefaults.standard.set(true, forKey: "turnedOnBluetooth")
|
||||
}
|
||||
|
||||
func sensorManagerDidUpdateBluetoothCallback(_ state: SensorState, type: SensorType?) {
|
||||
DLog("Bluetooth state changed in permission request to \(state.rawValue)")
|
||||
requestPushPermissions()
|
||||
}
|
||||
|
||||
func sensorManagerDidUpdateLocationCallback(_ state: SensorState, type: SensorType?) {
|
||||
DLog("Location state changed in permission request to \(state.rawValue)")
|
||||
|
||||
UserDefaults.standard.set(true, forKey: "allowedPermissions")
|
||||
BluetraceManager.shared.sensorDidUpdateStateCallback = self.backupSensorDidUpdateStateCallback
|
||||
DispatchQueue.main.async {
|
||||
self.performSegue(withIdentifier: "showSuccessSegue", sender: self)
|
||||
}
|
||||
}
|
||||
|
||||
func requestPushPermissions() {
|
||||
|
||||
UNUserNotificationCenter.current()
|
||||
.requestAuthorization(options: [.alert, .sound, .badge]) {
|
||||
granted, error in
|
||||
print("Permissions granted: \(granted)")
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
BluetraceManager.shared.sensorDidUpdateStateCallback = self.sensorManagerDidUpdateLocationCallback
|
||||
BluetraceManager.shared.turnOnLocationSensor()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="8X4-AC-7cb">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="8X4-AC-7cb">
|
||||
<device id="retina5_5" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Onboarding Step 2b View Controller-->
|
||||
<!--Registration Success View Controller-->
|
||||
<scene sceneID="PoE-0G-Ttd">
|
||||
<objects>
|
||||
<viewController modalTransitionStyle="crossDissolve" modalPresentationStyle="fullScreen" id="GaQ-f5-ei6" userLabel="Onboarding Step 2b View Controller" customClass="OnboardingStep2bViewController" customModule="COVIDSafe" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController modalTransitionStyle="crossDissolve" modalPresentationStyle="fullScreen" id="GaQ-f5-ei6" customClass="RegistrationSuccessViewController" customModule="COVIDSafe" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Dwh-jN-1SX">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
|
@ -197,7 +197,7 @@
|
|||
<!--Onboarding Step 1 View Controller-->
|
||||
<scene sceneID="26D-Zd-57d">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="onboardingStep1" modalTransitionStyle="crossDissolve" modalPresentationStyle="fullScreen" id="8nR-hO-fWt" userLabel="Onboarding Step 1 View Controller" customClass="OnboardingStep1ViewController" customModule="COVIDSafe" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController storyboardIdentifier="onboardingStep1" modalTransitionStyle="crossDissolve" modalPresentationStyle="fullScreen" id="8nR-hO-fWt" userLabel="Onboarding Step 1 View Controller" customClass="RegistrationIntroViewController" customModule="COVIDSafe" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="lOU-q5-7Co">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
|
@ -215,7 +215,7 @@
|
|||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Together we can stop the spread of COVID-19" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="00h-JA-MfK">
|
||||
<rect key="frame" x="32" y="208" width="308.33333333333331" height="67.666666666666686"/>
|
||||
<rect key="frame" x="32" y="208" width="309.66666666666669" height="67.666666666666686"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" staticText="YES" header="YES"/>
|
||||
</accessibility>
|
||||
|
@ -322,10 +322,10 @@ Together we can help stop the spread and stay healthy.</string>
|
|||
</objects>
|
||||
<point key="canvasLocation" x="-6738" y="-1212"/>
|
||||
</scene>
|
||||
<!--Onboarding Step 2 View Controller-->
|
||||
<!--App Settings View Controller-->
|
||||
<scene sceneID="d1g-JU-eoU">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="onboardingStep2" modalTransitionStyle="crossDissolve" modalPresentationStyle="fullScreen" id="eME-NJ-Fcz" userLabel="Onboarding Step 2 View Controller" customClass="OnboardingStep2ViewController" customModule="COVIDSafe" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController storyboardIdentifier="onboardingStep2" modalTransitionStyle="crossDissolve" modalPresentationStyle="fullScreen" id="eME-NJ-Fcz" customClass="AppSettingsViewController" customModule="COVIDSafe" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="dJg-Mo-aen">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
|
@ -334,8 +334,14 @@ Together we can help stop the spread and stay healthy.</string>
|
|||
<rect key="frame" x="0.0" y="0.0" width="414" height="624"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="SpU-i1-aUA" userLabel="ContentView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="617.66666666666663"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="635.33333333333337"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Step 4 of 4" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Vco-tH-Zts">
|
||||
<rect key="frame" x="32" y="60.000000000000007" width="350" height="19.333333333333336"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="AppPermissions1" translatesAutoresizingMaskIntoConstraints="NO" id="cma-Te-Ut1">
|
||||
<rect key="frame" x="51.666666666666657" y="103.33333333333331" width="311" height="188"/>
|
||||
<constraints>
|
||||
|
@ -355,7 +361,7 @@ Together we can help stop the spread and stay healthy.</string>
|
|||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="npY-9G-r2u">
|
||||
<rect key="frame" x="32" y="365" width="350" height="130.33333333333337"/>
|
||||
<rect key="frame" x="32" y="365" width="350" height="148"/>
|
||||
<string key="text">COVIDSafe needs Bluetooth® enabled to work. By enabling Notifications, you get updates to remind you when COVIDSafe is not active.
|
||||
|
||||
Select ‘Proceed' to enable:</string>
|
||||
|
@ -367,8 +373,22 @@ Select ‘Proceed' to enable:</string>
|
|||
<userDefinedRuntimeAttribute type="string" keyPath="localVOLabelKey" value="permission_content_iOS_VOLabel"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" editable="NO" textAlignment="natural" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vam-jJ-7TM">
|
||||
<rect key="frame" x="32" y="365" width="350" height="148"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<string key="text">COVIDSafe needs Bluetooth® enabled to work. By enabling Notifications, you get updates to remind you when COVIDSafe is not active.
|
||||
|
||||
Select ‘Proceed' to enable:</string>
|
||||
<color key="textColor" systemColor="labelColor"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="permission_content_iOS"/>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="localVOLabelKey" value="permission_content_iOS_VOLabel"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="DAg-5r-Sgd">
|
||||
<rect key="frame" x="32" y="515.33333333333337" width="350" height="86.333333333333371"/>
|
||||
<rect key="frame" x="32" y="533" width="350" height="86.333333333333371"/>
|
||||
<string key="text">1. Bluetooth®
|
||||
2. Notifications
|
||||
|
||||
|
@ -381,23 +401,20 @@ COVIDSafe does not send pairing requests.</string>
|
|||
<userDefinedRuntimeAttribute type="string" keyPath="localVOLabelKey" value="permission_content_iOS_2_VOLabel"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Step 4 of 4" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Vco-tH-Zts">
|
||||
<rect key="frame" x="32" y="60.000000000000007" width="350" height="19.333333333333336"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="npY-9G-r2u" firstAttribute="leading" secondItem="SpU-i1-aUA" secondAttribute="leading" constant="32" id="1qx-Y1-2Bz"/>
|
||||
<constraint firstItem="Vco-tH-Zts" firstAttribute="top" secondItem="SpU-i1-aUA" secondAttribute="top" constant="60" id="A22-Zs-PDI"/>
|
||||
<constraint firstAttribute="bottom" secondItem="DAg-5r-Sgd" secondAttribute="bottom" constant="16" id="ByQ-uv-A4X"/>
|
||||
<constraint firstItem="vam-jJ-7TM" firstAttribute="leading" secondItem="SpU-i1-aUA" secondAttribute="leading" constant="32" id="IgC-l4-Hyw"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Vco-tH-Zts" secondAttribute="trailing" constant="32" id="OUF-2C-mUH"/>
|
||||
<constraint firstItem="npY-9G-r2u" firstAttribute="top" secondItem="0pO-gl-umK" secondAttribute="bottom" constant="16" id="R2m-S5-4HK"/>
|
||||
<constraint firstItem="Vco-tH-Zts" firstAttribute="leading" secondItem="SpU-i1-aUA" secondAttribute="leading" constant="32" id="SNT-kH-ef3"/>
|
||||
<constraint firstAttribute="trailing" secondItem="DAg-5r-Sgd" secondAttribute="trailing" constant="32" id="SkE-oy-hPa"/>
|
||||
<constraint firstAttribute="trailing" secondItem="vam-jJ-7TM" secondAttribute="trailing" constant="32" id="ZSg-wl-Els"/>
|
||||
<constraint firstItem="DAg-5r-Sgd" firstAttribute="top" secondItem="npY-9G-r2u" secondAttribute="bottom" constant="20" id="c13-ir-irb"/>
|
||||
<constraint firstItem="DAg-5r-Sgd" firstAttribute="top" secondItem="vam-jJ-7TM" secondAttribute="bottom" constant="20" id="cEL-Fy-qkJ"/>
|
||||
<constraint firstItem="cma-Te-Ut1" firstAttribute="centerX" secondItem="SpU-i1-aUA" secondAttribute="centerX" id="cEy-KQ-T2p"/>
|
||||
<constraint firstAttribute="trailing" secondItem="npY-9G-r2u" secondAttribute="trailing" constant="32" id="eEl-PJ-NbS"/>
|
||||
<constraint firstItem="0pO-gl-umK" firstAttribute="top" secondItem="cma-Te-Ut1" secondAttribute="bottom" constant="24" id="eqm-MY-nO8"/>
|
||||
|
@ -405,6 +422,7 @@ COVIDSafe does not send pairing requests.</string>
|
|||
<constraint firstItem="DAg-5r-Sgd" firstAttribute="leading" secondItem="SpU-i1-aUA" secondAttribute="leading" constant="32" id="qRs-1i-saU"/>
|
||||
<constraint firstItem="cma-Te-Ut1" firstAttribute="top" secondItem="Vco-tH-Zts" secondAttribute="bottom" constant="24" id="tAa-dy-jEx"/>
|
||||
<constraint firstAttribute="trailing" secondItem="0pO-gl-umK" secondAttribute="trailing" constant="32" id="tnK-TI-UxL"/>
|
||||
<constraint firstItem="vam-jJ-7TM" firstAttribute="top" secondItem="0pO-gl-umK" secondAttribute="bottom" constant="16" id="yHr-8k-q7c"/>
|
||||
</constraints>
|
||||
<variation key="heightClass=compact-widthClass=compact">
|
||||
<mask key="subviews">
|
||||
|
@ -460,6 +478,7 @@ COVIDSafe does not send pairing requests.</string>
|
|||
<nil key="simulatedTopBarMetrics"/>
|
||||
<connections>
|
||||
<outlet property="stepCounterLabel" destination="Vco-tH-Zts" id="NsV-Af-g7D"/>
|
||||
<outlet property="topContentTextView" destination="vam-jJ-7TM" id="gPO-BM-gLt"/>
|
||||
<segue destination="GaQ-f5-ei6" kind="show" identifier="showSuccessSegue" id="Fee-nc-b99"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
|
@ -1257,10 +1276,10 @@ and save lives.</string>
|
|||
</objects>
|
||||
<point key="canvasLocation" x="-8561" y="1143"/>
|
||||
</scene>
|
||||
<!--Onboarding Step 1b View Controller-->
|
||||
<!--How It Works View Controller-->
|
||||
<scene sceneID="f3t-QQ-sex">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="onboardingStep1b" modalTransitionStyle="crossDissolve" modalPresentationStyle="fullScreen" useStoryboardIdentifierAsRestorationIdentifier="YES" id="szA-K1-DiB" userLabel="Onboarding Step 1b View Controller" customClass="OnboardingStep1bViewController" customModule="COVIDSafe" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController storyboardIdentifier="onboardingStep1b" modalTransitionStyle="crossDissolve" modalPresentationStyle="fullScreen" useStoryboardIdentifierAsRestorationIdentifier="YES" id="szA-K1-DiB" customClass="HowItWorksViewController" customModule="COVIDSafe" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="e8O-pH-ICg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
|
@ -1597,10 +1616,10 @@ For more information please refer to the *Help Topics* page.</string>
|
|||
</objects>
|
||||
<point key="canvasLocation" x="-3633.3333333333335" y="-1212.2282608695652"/>
|
||||
</scene>
|
||||
<!--Onboarding Step 1a View Controller-->
|
||||
<!--Privacy Policy View Controller-->
|
||||
<scene sceneID="R6W-kj-4rq">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="onboardingStep1a" modalTransitionStyle="crossDissolve" modalPresentationStyle="fullScreen" useStoryboardIdentifierAsRestorationIdentifier="YES" id="JMg-fD-wpp" customClass="OnboardingStep1aViewController" customModule="COVIDSafe" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController storyboardIdentifier="onboardingStep1a" modalTransitionStyle="crossDissolve" modalPresentationStyle="fullScreen" useStoryboardIdentifierAsRestorationIdentifier="YES" id="JMg-fD-wpp" userLabel="Privacy Policy View Controller" customClass="PrivacyPolicyViewController" customModule="COVIDSafe" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="NCU-xg-1ta">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
|
@ -2261,9 +2280,9 @@ See the COVIDSafe *privacy policy* for further details about your rights about y
|
|||
</scene>
|
||||
</scenes>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="bMl-IY-pjw"/>
|
||||
<segue reference="4eL-1Z-MtW"/>
|
||||
<segue reference="dge-8I-Ehz"/>
|
||||
<segue reference="rHc-rZ-59x"/>
|
||||
<segue reference="XiO-Zp-pOg"/>
|
||||
<segue reference="Fee-nc-b99"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
<image name="AppPermissions1" width="311" height="188"/>
|
||||
|
|
|
@ -16,7 +16,10 @@ struct BluetraceConfig {
|
|||
static let ProtocolVersion = 2
|
||||
|
||||
static let CentralScanInterval = 60.0 // in seconds
|
||||
static let PayloadExpiry = 20.0 // in seconds
|
||||
static let PeripheralPayloadExpiry = TimeInterval.minute * 5
|
||||
static let PeripheralCleanInterval = 120.0 // in seconds
|
||||
static let PeripheralPayloadSaveInterval = 30.0 // in seconds
|
||||
static let CentralScanDuration = 10 // in seconds
|
||||
|
||||
static let DummyModel = ""
|
||||
|
|
|
@ -2,24 +2,57 @@ import UIKit
|
|||
import CoreData
|
||||
import CoreBluetooth
|
||||
|
||||
class BluetraceManager {
|
||||
private var peripheralController: PeripheralController!
|
||||
private var centralController: CentralController!
|
||||
var queue: DispatchQueue!
|
||||
var bluetoothDidUpdateStateCallback: ((CBManagerState) -> Void)?
|
||||
class BluetraceManager: SensorDelegate {
|
||||
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Herald", category: "BluetraceManager")
|
||||
|
||||
var sensorDidUpdateStateCallback: ((SensorState, SensorType?) -> Void)?
|
||||
|
||||
// Payload data supplier, sensor and contact log
|
||||
var payloadDataSupplier: PayloadDataSupplier?
|
||||
var sensor: Sensor?
|
||||
|
||||
static let shared = BluetraceManager()
|
||||
|
||||
private init() {
|
||||
queue = DispatchQueue(label: "BluetraceManager")
|
||||
peripheralController = PeripheralController(peripheralName: "TR", queue: queue)
|
||||
centralController = CentralController(queue: queue)
|
||||
centralController.centralDidUpdateStateCallback = centralDidUpdateStateCallback
|
||||
var bleSensorState: SensorState = .unavailable
|
||||
var awakeSensorState: SensorState = .unavailable
|
||||
|
||||
private var didWritePayloadData: [String:Date] = [:]
|
||||
|
||||
func turnOnBLE() {
|
||||
payloadDataSupplier = EncounterMessageManager.shared
|
||||
if sensor == nil {
|
||||
sensor = SensorArray(payloadDataSupplier!)
|
||||
sensor?.add(delegate: self)
|
||||
}
|
||||
sensor?.start()
|
||||
}
|
||||
|
||||
func turnOn() {
|
||||
peripheralController.turnOn()
|
||||
centralController.turnOn()
|
||||
func turnOnAllSensors() {
|
||||
payloadDataSupplier = EncounterMessageManager.shared
|
||||
if sensor == nil {
|
||||
sensor = SensorArray(payloadDataSupplier!)
|
||||
sensor?.add(delegate: self)
|
||||
// we are going to turn on one at a time
|
||||
let previousSensorDidUpdateStateCallback = sensorDidUpdateStateCallback
|
||||
sensorDidUpdateStateCallback = { state, type in
|
||||
if (state == .on ) {
|
||||
self.sensorDidUpdateStateCallback = previousSensorDidUpdateStateCallback
|
||||
self.turnOnLocationSensor()
|
||||
}
|
||||
}
|
||||
}
|
||||
sensor?.start()
|
||||
}
|
||||
|
||||
|
||||
func turnOnLocationSensor() {
|
||||
guard let sensorArray = sensor as? SensorArray else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
sensorArray.startAwakeSensor()
|
||||
}
|
||||
}
|
||||
|
||||
func isBluetoothAuthorized() -> Bool {
|
||||
|
@ -31,26 +64,127 @@ class BluetraceManager {
|
|||
}
|
||||
|
||||
func isBluetoothOn() -> Bool {
|
||||
return centralController.getState() == .poweredOn
|
||||
return bleSensorState == .on
|
||||
}
|
||||
|
||||
func isLocationOnAuthorized() -> Bool {
|
||||
return awakeSensorState == .on
|
||||
}
|
||||
|
||||
func centralDidUpdateStateCallback(_ state: CBManagerState) {
|
||||
bluetoothDidUpdateStateCallback?(state)
|
||||
if state == .poweredOn {
|
||||
sensorDidUpdateStateCallback?(.on, .BLE)
|
||||
} else {
|
||||
sensorDidUpdateStateCallback?(.off, .BLE)
|
||||
}
|
||||
}
|
||||
|
||||
func toggleAdvertisement(_ state: Bool) {
|
||||
if state {
|
||||
peripheralController.turnOn()
|
||||
sensor?.start()
|
||||
} else {
|
||||
peripheralController.turnOff()
|
||||
sensor?.stop()
|
||||
}
|
||||
}
|
||||
|
||||
func toggleScanning(_ state: Bool) {
|
||||
if state {
|
||||
centralController.turnOn()
|
||||
sensor?.start()
|
||||
} else {
|
||||
centralController.turnOff()
|
||||
sensor?.stop()
|
||||
}
|
||||
}
|
||||
|
||||
func cleanDidWritePayloadData() {
|
||||
let removeDataBefore = Date() - BluetraceConfig.PeripheralPayloadExpiry
|
||||
let recentDidWritePayloadData = didWritePayloadData.filter({ $0.value >= removeDataBefore })
|
||||
didWritePayloadData = recentDidWritePayloadData
|
||||
}
|
||||
|
||||
func saveEncounter(payload: PayloadData, proximity: Proximity, txPower: Int? ) throws {
|
||||
let peripheralCharData = try JSONDecoder().decode(PeripheralCharacteristicsData.self, from: payload)
|
||||
var encounterStruct = EncounterRecord(rssi: proximity.value, txPower: txPower == nil ? nil : Double(txPower!))
|
||||
encounterStruct.msg = peripheralCharData.msg
|
||||
encounterStruct.update(modelP: peripheralCharData.modelP)
|
||||
encounterStruct.org = peripheralCharData.org
|
||||
encounterStruct.v = peripheralCharData.v
|
||||
encounterStruct.timestamp = Date()
|
||||
// here the remote blob will be msg and modelp if v1, msg if v2
|
||||
// local blob will be rssi, txpower, modelc
|
||||
try encounterStruct.saveRemotePeripheralToCoreData()
|
||||
}
|
||||
|
||||
func getIdentifier(forDevice: BLEDevice) -> String {
|
||||
return forDevice.pseudoDeviceAddress != nil ? "\(forDevice.pseudoDeviceAddress!.address)" : forDevice.identifier
|
||||
}
|
||||
|
||||
func shouldSaveEncounter(forDeviceIdentifier: String) -> Bool {
|
||||
|
||||
guard didWritePayloadData[forDeviceIdentifier] == nil || abs(didWritePayloadData[forDeviceIdentifier]!.timeIntervalSinceNow) > BluetraceConfig.PeripheralPayloadSaveInterval else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK:- SensorDelegate
|
||||
|
||||
func sensor(_ sensor: SensorType, didDetect: TargetIdentifier) {
|
||||
logger.info(sensor.rawValue + ",didDetect=" + didDetect.description)
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier) {
|
||||
logger.info(sensor.rawValue + ",didMeasure=" + didMeasure.description + ",fromTarget=" + fromTarget.description)
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier, withPayload: PayloadData, forDevice: BLEDevice) {
|
||||
logger.info(sensor.rawValue + ",didMeasure=" + didMeasure.description + ",fromTarget=" + fromTarget.description + ",withPayload=" + withPayload.shortName)
|
||||
|
||||
let deviceIdentifier = getIdentifier(forDevice: forDevice)
|
||||
guard shouldSaveEncounter(forDeviceIdentifier: deviceIdentifier) else {
|
||||
logger.info("didMeasure, skipping save as time since last saved too recet fromTarget=" + fromTarget.description)
|
||||
return
|
||||
}
|
||||
didWritePayloadData[deviceIdentifier] = Date()
|
||||
cleanDidWritePayloadData()
|
||||
do {
|
||||
logger.debug("Saving on didMeasure fromTarget=" + fromTarget.description)
|
||||
try saveEncounter(payload: withPayload, proximity: didMeasure, txPower: nil)
|
||||
} catch {
|
||||
logger.fault("ERROR "+sensor.rawValue + ",didMeasure=" + didMeasure.description + ",fromTarget=" + fromTarget.description + ",withPayload=" + withPayload.shortName )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier) {
|
||||
do {
|
||||
let dataFromCentral = try JSONDecoder().decode(CentralWriteData.self, from: didRead)
|
||||
logger.info(sensor.rawValue + ",didRead=" + dataFromCentral.msg + ",fromTarget=" + fromTarget.description)
|
||||
} catch {
|
||||
logger.fault(sensor.rawValue + ",didRead=" + didRead.shortName + ",fromTarget=" + fromTarget.description)
|
||||
}
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier, atProximity: Proximity, withTxPower: Int?) {
|
||||
logger.info(sensor.rawValue + ",didRead=\(didRead.shortName))" + ",fromTarget=" + fromTarget.description + ",atProximity=" + atProximity.description + ",withTxPower=\(String(describing: withTxPower))")
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier, atProximity: Proximity) {
|
||||
let payloads = didShare.map { $0.shortName }
|
||||
logger.info(sensor.rawValue + ",didShare=" + payloads.description + ",fromTarget=" + fromTarget.description)
|
||||
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didUpdateState: SensorState) {
|
||||
logger.info(sensor.rawValue + ",didUpdateState=" + didUpdateState.rawValue)
|
||||
|
||||
if sensor == .BLE {
|
||||
bleSensorState = didUpdateState
|
||||
sensorDidUpdateStateCallback?(didUpdateState, sensor)
|
||||
}
|
||||
|
||||
if sensor == .AWAKE {
|
||||
awakeSensorState = didUpdateState
|
||||
sensorDidUpdateStateCallback?(didUpdateState, sensor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
//
|
||||
// BluetraceUtils.swift
|
||||
// CovidSafe
|
||||
//
|
||||
// Copyright © 2020 Australian Government. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
class BluetraceUtils {
|
||||
static func centralStateToString(_ state: CBManagerState) -> String {
|
||||
switch state {
|
||||
case .poweredOff:
|
||||
return "poweredOff"
|
||||
case .poweredOn:
|
||||
return "poweredOn"
|
||||
case .resetting:
|
||||
return "resetting"
|
||||
case .unauthorized:
|
||||
return "unauthorized"
|
||||
case .unknown:
|
||||
return "unknown"
|
||||
case .unsupported:
|
||||
return "unsupported"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
static func peripheralStateToString(_ state: CBPeripheralState) -> String {
|
||||
switch state {
|
||||
case .disconnected:
|
||||
return "disconnected"
|
||||
case .connecting:
|
||||
return "connecting"
|
||||
case .connected:
|
||||
return "connected"
|
||||
case .disconnecting:
|
||||
return "disconnecting"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
}
|
123
CovidSafe/CSGenericContentView.xib
Normal file
123
CovidSafe/CSGenericContentView.xib
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina5_5" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="CSGenericContentViewController" customModule="COVIDSafe" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="actionButton" destination="pS6-bO-9yg" id="sYw-k5-R8U"/>
|
||||
<outlet property="contentDescriptionText" destination="X8s-K8-VFq" id="YaF-PU-7SZ"/>
|
||||
<outlet property="contentIllustration" destination="GdB-8O-YjS" id="V3F-Vb-uDL"/>
|
||||
<outlet property="contentTitleLabel" destination="DwC-ld-pUl" id="OYv-w1-lOH"/>
|
||||
<outlet property="stepCounterLabel" destination="yhY-Mm-uhw" id="epJ-LV-aL5"/>
|
||||
<outlet property="view" destination="iN0-l3-epB" id="3J4-LH-feh"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="f5Y-Hn-xEY">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="623"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="PA9-VS-Ata" userLabel="ContentView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="881"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="24" translatesAutoresizingMaskIntoConstraints="NO" id="A0s-lP-iFl">
|
||||
<rect key="frame" x="32" y="24" width="350" height="857"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Step x of x" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="yhY-Mm-uhw">
|
||||
<rect key="frame" x="0.0" y="0.0" width="350" height="19.333333333333332"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="AppPermissions1" translatesAutoresizingMaskIntoConstraints="NO" id="GdB-8O-YjS">
|
||||
<rect key="frame" x="0.0" y="43.333333333333314" width="350" height="188"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="188" id="cYY-sp-rPl"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Screen Title" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="DwC-ld-pUl">
|
||||
<rect key="frame" x="0.0" y="255.33333333333329" width="350" height="33.666666666666657"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" staticText="YES" header="YES"/>
|
||||
</accessibility>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle1"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" bouncesZoom="NO" editable="NO" textAlignment="natural" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="X8s-K8-VFq">
|
||||
<rect key="frame" x="0.0" y="313" width="350" height="544"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<color key="tintColor" red="0.0" green="0.40000000000000002" blue="0.1058823529" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<string key="text">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin vel sapien a sapien dignissim tempus.
Vestibulum ut ante ipsum. Vivamus lobortis hendrerit elit, non maximus nulla maximus in. Vivamus euismod elementum finibus.
Nulla leo leo, gravida vel enim in, ultricies fringilla orci. Duis tincidunt vel tortor quis pellentesque.
Fusce sit amet elit massa. Cras tellus tortor, convallis id vulputate sit amet, accumsan sit amet nisl.
|
||||
|
||||
Praesent viverra pretium lobortis. In laoreet at leo non viverra. Nullam sagittis nulla a lobortis auctor. Donec a pulvinar odio, eget eleifend velit. Maecenas vulputate eget libero at malesuada. Vestibulum orci nulla, consectetur ut lobortis a, tincidunt vel mauris. Integer et eros pharetra, porta sem et, rhoncus mauris. Duis porta semper malesuada.</string>
|
||||
<color key="textColor" systemColor="labelColor"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="A0s-lP-iFl" firstAttribute="leading" secondItem="PA9-VS-Ata" secondAttribute="leading" constant="32" id="CD7-gV-If4"/>
|
||||
<constraint firstItem="A0s-lP-iFl" firstAttribute="top" secondItem="PA9-VS-Ata" secondAttribute="top" constant="24" id="SOd-SJ-DLZ"/>
|
||||
<constraint firstAttribute="bottom" secondItem="A0s-lP-iFl" secondAttribute="bottom" id="mxD-jf-tst"/>
|
||||
<constraint firstAttribute="trailing" secondItem="A0s-lP-iFl" secondAttribute="trailing" constant="32" id="pbp-mf-DUM"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="PA9-VS-Ata" secondAttribute="trailing" id="UKv-58-Gea"/>
|
||||
<constraint firstAttribute="bottom" secondItem="PA9-VS-Ata" secondAttribute="bottom" id="eq0-cS-sUs"/>
|
||||
<constraint firstItem="PA9-VS-Ata" firstAttribute="leading" secondItem="f5Y-Hn-xEY" secondAttribute="leading" id="f7i-qf-xzj"/>
|
||||
<constraint firstItem="PA9-VS-Ata" firstAttribute="top" secondItem="f5Y-Hn-xEY" secondAttribute="top" id="hBS-kh-b3c"/>
|
||||
<constraint firstItem="PA9-VS-Ata" firstAttribute="width" secondItem="f5Y-Hn-xEY" secondAttribute="width" id="kOg-xd-wbK"/>
|
||||
</constraints>
|
||||
</scrollView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="pS6-bO-9yg">
|
||||
<rect key="frame" x="32" y="655" width="350" height="49"/>
|
||||
<color key="backgroundColor" red="0.0" green="0.40000000000000002" blue="0.1058823529" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="49" id="hSQ-bH-GZs"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<state key="normal" title="Button Label">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</state>
|
||||
</button>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="pS6-bO-9yg" secondAttribute="bottom" constant="32" id="2o9-fJ-zq2"/>
|
||||
<constraint firstItem="pS6-bO-9yg" firstAttribute="top" secondItem="f5Y-Hn-xEY" secondAttribute="bottom" constant="32" id="AWh-yg-NLr"/>
|
||||
<constraint firstItem="f5Y-Hn-xEY" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="BNc-2U-Sh4"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="pS6-bO-9yg" secondAttribute="trailing" constant="32" id="J7k-aP-oh5"/>
|
||||
<constraint firstItem="f5Y-Hn-xEY" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="dPk-ul-WHy"/>
|
||||
<constraint firstItem="f5Y-Hn-xEY" firstAttribute="trailing" secondItem="vUN-kp-3ea" secondAttribute="trailing" id="ftn-U3-tkK"/>
|
||||
<constraint firstItem="pS6-bO-9yg" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="32" id="gc9-xx-gfg"/>
|
||||
</constraints>
|
||||
<point key="canvasLocation" x="140.57971014492756" y="78.348214285714278"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="AppPermissions1" width="311" height="188"/>
|
||||
<systemColor name="labelColor">
|
||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
73
CovidSafe/CSGenericViewController.swift
Normal file
73
CovidSafe/CSGenericViewController.swift
Normal file
|
@ -0,0 +1,73 @@
|
|||
//
|
||||
// CSGenericContentViewController.swift
|
||||
// CovidSafe
|
||||
//
|
||||
// Copyright © 2020 Australian Government. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SafariServices
|
||||
|
||||
class CSGenericContentViewController: UIViewController {
|
||||
|
||||
@IBOutlet weak var stepCounterLabel: UILabel!
|
||||
@IBOutlet weak var contentIllustration: UIImageView!
|
||||
@IBOutlet weak var contentTitleLabel: UILabel!
|
||||
@IBOutlet weak var contentDescriptionText: UITextView!
|
||||
@IBOutlet weak var actionButton: UIButton!
|
||||
|
||||
var contentViewModel: CSGenericContentViewModel?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
guard let viewModel = contentViewModel else {
|
||||
return
|
||||
}
|
||||
|
||||
// set the step counter
|
||||
if let contentStep = viewModel.contentStepNumber, let contentTotal = viewModel.contentStepTotal {
|
||||
stepCounterLabel.text = String.localizedStringWithFormat( "stepCounter".localizedString(),
|
||||
contentStep,
|
||||
contentTotal
|
||||
)
|
||||
} else {
|
||||
stepCounterLabel.text = ""
|
||||
stepCounterLabel.isHidden = true
|
||||
}
|
||||
|
||||
// set the illustration
|
||||
if let illustration = viewModel.contentIllustration {
|
||||
contentIllustration.image = illustration
|
||||
}
|
||||
|
||||
// set title and content
|
||||
contentTitleLabel.text = viewModel.viewTitle
|
||||
contentDescriptionText.attributedText = viewModel.viewContentDescription
|
||||
contentDescriptionText.parseHTMLTags()
|
||||
contentDescriptionText.addAllBold(enclosedIn: "#")
|
||||
|
||||
//set button label and action
|
||||
actionButton.setTitle(viewModel.buttonLabel, for: .normal)
|
||||
actionButton.addTarget(self, action: #selector(pressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func pressed(sender: UIButton!) {
|
||||
guard let viewModel = contentViewModel else {
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.buttonCallback()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct CSGenericContentViewModel {
|
||||
var viewTitle: String
|
||||
var viewContentDescription: NSAttributedString
|
||||
var buttonLabel: String
|
||||
var buttonCallback: () -> Void
|
||||
var contentIllustration: UIImage?
|
||||
var contentStepNumber: Int?
|
||||
var contentStepTotal: Int?
|
||||
}
|
|
@ -1,496 +0,0 @@
|
|||
//
|
||||
// CentralController.swift
|
||||
// CovidSafe
|
||||
//
|
||||
// Copyright © 2020 Australian Government. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
import CoreData
|
||||
import CoreBluetooth
|
||||
import UIKit
|
||||
|
||||
struct CentralWriteData: Codable {
|
||||
var modelC: String // phone model of central
|
||||
var rssi: Double
|
||||
var txPower: Double?
|
||||
var msg: String // tempID
|
||||
var org: String
|
||||
var v: Int
|
||||
}
|
||||
|
||||
class CentralController: NSObject {
|
||||
enum CentralError: Error {
|
||||
case centralAlreadyOn
|
||||
case centralAlreadyOff
|
||||
}
|
||||
var centralDidUpdateStateCallback: ((CBManagerState) -> Void)?
|
||||
var characteristicDidReadValue: ((EncounterRecord) -> Void)?
|
||||
private let restoreIdentifierKey = "com.joelkek.tracer.central"
|
||||
private var central: CBCentralManager?
|
||||
private var recoveredPeripherals: [CBPeripheral] = []
|
||||
private var cleanupPeripherals: [CBPeripheral] = []
|
||||
private var queue: DispatchQueue
|
||||
|
||||
// This dict is to keep track of discovered android devices, so that i do not connect to the same android device multiple times within the same BluetraceConfig.CentralScanInterval
|
||||
private var discoveredAndroidPeriManufacturerToUUIDMap = [Data: UUID]()
|
||||
|
||||
// This dict has 2 purpose
|
||||
// 1. To store all the EncounterRecord, because the RSSI and TxPower is gotten at the didDiscoverPeripheral delegate, but the characterstic value is gotten at didUpdateValueForCharacteristic delegate
|
||||
// 2. Use to check for duplicated iphones peripheral being discovered, so that i dont connect to the same iphone again in the same scan window
|
||||
private var scannedPeripherals = [UUID: (lastUpdated: Date, peripheral: CBPeripheral, encounter: EncounterRecord)]() // stores the peripherals encountered within one scan interval
|
||||
var timerForScanning: Timer?
|
||||
private var lastCleanedScannedPeripherals = Date()
|
||||
|
||||
public init(queue: DispatchQueue) {
|
||||
self.queue = queue
|
||||
|
||||
super.init()
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: UIApplication.didReceiveMemoryWarningNotification,
|
||||
object: nil,
|
||||
queue: .main) { [weak self] notification in
|
||||
self?.queue.async {
|
||||
self?.cleanupScannedPeripherals()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func turnOn() {
|
||||
DLog("CC requested to be turnOn")
|
||||
guard central == nil else {
|
||||
return
|
||||
}
|
||||
let options: [String: Any] = [CBCentralManagerOptionRestoreIdentifierKey: restoreIdentifierKey,
|
||||
CBCentralManagerOptionShowPowerAlertKey: NSNumber(true)]
|
||||
central = CBCentralManager(delegate: self, queue: self.queue, options: options )
|
||||
}
|
||||
|
||||
func turnOff() {
|
||||
DLog("CC turnOff")
|
||||
guard central != nil else {
|
||||
return
|
||||
}
|
||||
central?.stopScan()
|
||||
central = nil
|
||||
}
|
||||
|
||||
func shouldRecordEncounter(_ encounter: EncounterRecord) -> Bool {
|
||||
guard let scannedDate = encounter.timestamp else {
|
||||
DLog("Not recorded encounter before \(encounter)")
|
||||
return true
|
||||
}
|
||||
if abs(scannedDate.timeIntervalSinceNow) > BluetraceConfig.CentralScanInterval {
|
||||
DLog("Encounter last recorded \(abs(scannedDate.timeIntervalSinceNow)) seconds ago")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldReconnectToPeripheral(peripheral: CBPeripheral) -> Bool {
|
||||
guard peripheral.state == .disconnected else {
|
||||
return false
|
||||
}
|
||||
guard let encounteredPeripheral = scannedPeripherals[peripheral.identifier] else {
|
||||
DLog("Not previously encountered CBPeripheral \(String(describing: peripheral.name))")
|
||||
return true
|
||||
}
|
||||
guard let scannedDate = encounteredPeripheral.encounter.timestamp else {
|
||||
DLog("Not previously recorded an encounter with \(encounteredPeripheral)")
|
||||
return true
|
||||
}
|
||||
if abs(scannedDate.timeIntervalSinceNow) > BluetraceConfig.CentralScanInterval {
|
||||
DLog("Peripheral last recorded \(abs(scannedDate.timeIntervalSinceNow)) seconds ago")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func getState() -> CBManagerState? {
|
||||
return central?.state
|
||||
}
|
||||
|
||||
public func logPeripheralsCount(description: String) {
|
||||
#if DEBUG
|
||||
guard let peripherals = central?.retrieveConnectedPeripherals(withServices: [BluetraceConfig.BluetoothServiceID]) else {
|
||||
return
|
||||
}
|
||||
|
||||
var connected = 0
|
||||
var connecting = 0
|
||||
var disconnected = 0
|
||||
var disconnecting = 0
|
||||
var unknown = 0
|
||||
|
||||
for peripheral in peripherals {
|
||||
switch peripheral.state {
|
||||
case .connecting:
|
||||
connecting+=1
|
||||
case .connected:
|
||||
connected+=1
|
||||
case .disconnected:
|
||||
disconnected+=1
|
||||
case .disconnecting:
|
||||
disconnecting+=1
|
||||
default:
|
||||
unknown+=1
|
||||
}
|
||||
}
|
||||
|
||||
let bleLogStr = "CC \(description) Current peripherals \nconnected: \(connected), \nconnecting: \(connecting), \ndisconnected: \(disconnected), \ndisconnecting: \(disconnecting), \nunknown: \(unknown), \nscannedPeripherals: \(scannedPeripherals.count)"
|
||||
let logRecord = BLELogRecord(message: bleLogStr)
|
||||
logRecord.saveToCoreData()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
extension CentralController: CBCentralManagerDelegate {
|
||||
|
||||
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
|
||||
DLog("CC willRestoreState. Central state: \(BluetraceUtils.centralStateToString(central.state))")
|
||||
recoveredPeripherals = []
|
||||
if let peripheralsObject = dict[CBCentralManagerRestoredStatePeripheralsKey] {
|
||||
let peripherals = peripheralsObject as! Array<CBPeripheral>
|
||||
DLog("CC restoring \(peripherals.count) peripherals from system.")
|
||||
logPeripheralsCount(description: "Restoring peripherals")
|
||||
for peripheral in peripherals {
|
||||
if peripheral.state == .connected {
|
||||
// only recover connected peripherals, dispose/disconnect otherwise.
|
||||
recoveredPeripherals.append(peripheral)
|
||||
peripheral.delegate = self
|
||||
} else {
|
||||
cleanupPeripherals.append(peripheral)
|
||||
}
|
||||
}
|
||||
logPeripheralsCount(description: "Done Restoring peripherals")
|
||||
}
|
||||
}
|
||||
|
||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
centralDidUpdateStateCallback?(central.state)
|
||||
switch central.state {
|
||||
case .poweredOn:
|
||||
DLog("CC Starting a scan")
|
||||
|
||||
// for all peripherals that are not disconnected, disconnect them
|
||||
self.scannedPeripherals.forEach { (scannedPeri) in
|
||||
central.cancelPeripheralConnection(scannedPeri.value.peripheral)
|
||||
}
|
||||
// clear all peripherals, such that a new scan window can take place
|
||||
self.scannedPeripherals = [UUID: (Date, CBPeripheral, EncounterRecord)]()
|
||||
self.discoveredAndroidPeriManufacturerToUUIDMap = [Data: UUID]()
|
||||
// handle a state restoration scenario
|
||||
for recoveredPeripheral in recoveredPeripherals {
|
||||
var restoredEncounter = EncounterRecord(rssi: 0, txPower: nil)
|
||||
restoredEncounter.timestamp = nil
|
||||
scannedPeripherals.updateValue((Date(), recoveredPeripheral, restoredEncounter),
|
||||
forKey: recoveredPeripheral.identifier)
|
||||
central.connect(recoveredPeripheral)
|
||||
}
|
||||
|
||||
// can't cancel peripheral when BL OFF
|
||||
for cleanupPeripheral in self.cleanupPeripherals {
|
||||
central.cancelPeripheralConnection(cleanupPeripheral)
|
||||
}
|
||||
self.cleanupPeripherals = []
|
||||
|
||||
central.scanForPeripherals(withServices: [BluetraceConfig.BluetoothServiceID], options:nil)
|
||||
logPeripheralsCount(description: "Update state powerOn")
|
||||
default:
|
||||
DLog("State chnged to \(central.state)")
|
||||
}
|
||||
}
|
||||
|
||||
func handlePeripheralOfUncertainStatus(_ peripheral: CBPeripheral) {
|
||||
// If not connected to Peripheral, attempt connection and exit
|
||||
if peripheral.state != .connected {
|
||||
DLog("CC handlePeripheralOfUncertainStatus not connected")
|
||||
central?.connect(peripheral)
|
||||
return
|
||||
}
|
||||
// If don't know about Peripheral's services, discover services and exit
|
||||
if peripheral.services == nil {
|
||||
DLog("CC handlePeripheralOfUncertainStatus unknown services")
|
||||
peripheral.discoverServices([BluetraceConfig.BluetoothServiceID])
|
||||
return
|
||||
}
|
||||
// If Peripheral's services don't contain targetID, disconnect and remove, then exit.
|
||||
// If it does contain targetID, discover characteristics for service
|
||||
guard let service = peripheral.services?.first(where: { $0.uuid == BluetraceConfig.BluetoothServiceID }) else {
|
||||
DLog("CC handlePeripheralOfUncertainStatus no matching Services")
|
||||
central?.cancelPeripheralConnection(peripheral)
|
||||
return
|
||||
}
|
||||
DLog("CC handlePeripheralOfUncertainStatus discoverCharacteristics")
|
||||
peripheral.discoverCharacteristics([BluetraceConfig.BluetoothServiceID], for: service)
|
||||
// If Peripheral's service's characteristics don't contain targetID, disconnect and remove, then exit.
|
||||
// If it does contain targetID, read value for characteristic
|
||||
guard let characteristic = service.characteristics?.first(where: { $0.uuid == BluetraceConfig.BluetoothServiceID}) else {
|
||||
DLog("CC handlePeripheralOfUncertainStatus no matching Characteristics")
|
||||
central?.cancelPeripheralConnection(peripheral)
|
||||
return
|
||||
}
|
||||
DLog("CC handlePeripheralOfUncertainStatus readValue")
|
||||
peripheral.readValue(for: characteristic)
|
||||
return
|
||||
}
|
||||
|
||||
fileprivate func cleanupScannedPeripherals() {
|
||||
|
||||
DLog("CC scannedPeripherals/pending connections cleanup \(scannedPeripherals.count)")
|
||||
scannedPeripherals = scannedPeripherals.filter { scannedPeripheral in
|
||||
// if has been sitting in scanned for over 2 mins, remove
|
||||
if abs(scannedPeripheral.value.lastUpdated.timeIntervalSinceNow) > BluetraceConfig.PeripheralCleanInterval {
|
||||
// expired timestamp, remove
|
||||
cleanupPeripherals.append(scannedPeripheral.value.peripheral)
|
||||
return false
|
||||
} else {
|
||||
// not yet expired timestamp, keep this scanned peripheral data
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// remove android manufacturer data where peripheral has been removed
|
||||
discoveredAndroidPeriManufacturerToUUIDMap = discoveredAndroidPeriManufacturerToUUIDMap.filter(
|
||||
{ andperi in
|
||||
return !scannedPeripherals.keys.contains(andperi.value)}
|
||||
)
|
||||
|
||||
|
||||
guard let central = central else {
|
||||
DLog("CC central is nil, unable to clean peripherals at the moment")
|
||||
return
|
||||
}
|
||||
|
||||
for cleanupPeripheral in cleanupPeripherals {
|
||||
central.cancelPeripheralConnection(cleanupPeripheral)
|
||||
}
|
||||
cleanupPeripherals = []
|
||||
lastCleanedScannedPeripherals = Date()
|
||||
DLog("CC post scannedPeripherals/pending connections cleanup \(scannedPeripherals.count)")
|
||||
return
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
||||
let debugLogs = ["CentralState": BluetraceUtils.centralStateToString(central.state),
|
||||
"peripheral": peripheral,
|
||||
"advertisments": advertisementData as AnyObject] as AnyObject
|
||||
|
||||
// dispatch in bluetrace queue
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
MessageAPI.getMessagesIfNeeded() { (messageResponse, error) in
|
||||
if let error = error {
|
||||
DLog("Get messages error: \(error.localizedDescription)")
|
||||
}
|
||||
// We currently dont do anything with the response. Messages are delivered via APN
|
||||
}
|
||||
}
|
||||
|
||||
DLog("\(debugLogs)")
|
||||
// regularly cleanup and close pending connections
|
||||
if abs(lastCleanedScannedPeripherals.timeIntervalSince(Date())) > BluetraceConfig.CentralScanInterval {
|
||||
cleanupScannedPeripherals()
|
||||
}
|
||||
|
||||
var initialEncounter = EncounterRecord(rssi: RSSI.doubleValue, txPower: advertisementData[CBAdvertisementDataTxPowerLevelKey] as? Double)
|
||||
initialEncounter.timestamp = nil
|
||||
|
||||
// iphones will "mask" the peripheral's identifier for android devices, resulting in the same android device being discovered multiple times with different peripheral identifier. Hence Android is using use CBAdvertisementDataServiceDataKey data for identifying an android pheripheral
|
||||
// Also, check that the length is greater than 2 to prevent crash. Otherwise ignore.
|
||||
if let manuData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data, manuData.count > 2 {
|
||||
let androidIdentifierData = manuData.subdata(in: 2..<manuData.count)
|
||||
if discoveredAndroidPeriManufacturerToUUIDMap.keys.contains(androidIdentifierData) {
|
||||
DLog("Android Peripheral \(peripheral) has been discovered already in this window, will not attempt to connect to it again")
|
||||
return
|
||||
} else {
|
||||
peripheral.delegate = self
|
||||
discoveredAndroidPeriManufacturerToUUIDMap.updateValue(peripheral.identifier, forKey: androidIdentifierData)
|
||||
scannedPeripherals.updateValue((Date(), peripheral, initialEncounter), forKey: peripheral.identifier)
|
||||
central.connect(peripheral)
|
||||
}
|
||||
} else {
|
||||
// Means not android device, i will check if the peripheral.identifier exist in the scannedPeripherals
|
||||
DLog("CBAdvertisementDataManufacturerDataKey Data not found. Peripheral is likely not android")
|
||||
logPeripheralsCount(description: "begin didDiscover iOS device")
|
||||
if let encounteredPeripheral = scannedPeripherals[peripheral.identifier] {
|
||||
if shouldReconnectToPeripheral(peripheral: encounteredPeripheral.peripheral) {
|
||||
peripheral.delegate = self
|
||||
central.connect(peripheral)
|
||||
DLog("found previous peripheral from more than 60 seconds ago")
|
||||
} else {
|
||||
DLog("iOS Peripheral \(peripheral) has been discovered already in this window, will not attempt to connect to it again")
|
||||
if let scannedDate = encounteredPeripheral.encounter.timestamp {
|
||||
DLog("It was found \(scannedDate.timeIntervalSinceNow) seconds ago")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
peripheral.delegate = self
|
||||
scannedPeripherals.updateValue((Date(), peripheral, initialEncounter), forKey: peripheral.identifier)
|
||||
central.connect(peripheral)
|
||||
}
|
||||
logPeripheralsCount(description: "finish didDiscover iOS device")
|
||||
}
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||
let peripheralStateString = BluetraceUtils.peripheralStateToString(peripheral.state)
|
||||
DLog("CC didConnect peripheral peripheralCentral state: \(BluetraceUtils.centralStateToString(central.state)), Peripheral state: \(peripheralStateString)")
|
||||
|
||||
guard let seenPeripheral = scannedPeripherals[peripheral.identifier], shouldRecordEncounter(seenPeripheral.encounter) else {
|
||||
central.cancelPeripheralConnection(peripheral)
|
||||
return
|
||||
}
|
||||
peripheral.delegate = self
|
||||
peripheral.readRSSI()
|
||||
peripheral.discoverServices([BluetraceConfig.BluetoothServiceID])
|
||||
logPeripheralsCount(description: "didConnect peripheral")
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
||||
DLog("CC didDisconnectPeripheral \(peripheral) , \(error != nil ? "error: \(error.debugDescription)" : "" )")
|
||||
|
||||
// We check that the periferal has been scanned and that there is no error.
|
||||
// No error indicates that cancelPeripheralConnection was called.
|
||||
// An error may represent that the peripheral is out of range, BL is OFF etc. In that case we don't want to retry.
|
||||
guard scannedPeripherals[peripheral.identifier] != nil && error == nil else {
|
||||
// Remove from scanned peripherals as got diconnected due to error. Also look after memory.
|
||||
scannedPeripherals.removeValue(forKey: peripheral.identifier)
|
||||
central.cancelPeripheralConnection(peripheral)
|
||||
return
|
||||
}
|
||||
|
||||
// only attempt to reconnect if the peripheral is in the scanned dictionary and there was no error.
|
||||
if #available(iOS 12, *) {
|
||||
let options = [CBConnectPeripheralOptionStartDelayKey: NSNumber(15)]
|
||||
central.connect(peripheral, options: options)
|
||||
}
|
||||
logPeripheralsCount(description: "didDisconnect peripheral")
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
||||
DLog("CC didFailToConnect peripheral \(error != nil ? "error: \(error.debugDescription)" : "" )")
|
||||
// Remove from scanned peripherals as connection failed. Also look after memory.
|
||||
scannedPeripherals.removeValue(forKey: peripheral.identifier)
|
||||
|
||||
// by cancelling the connection we are being extra sure the peripheral is fully
|
||||
// disconnected and not left in a pending state
|
||||
central.cancelPeripheralConnection(peripheral)
|
||||
logPeripheralsCount(description: "didFailToConnect peripheral")
|
||||
}
|
||||
}
|
||||
|
||||
extension CentralController: CBPeripheralDelegate {
|
||||
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
||||
if let err = error {
|
||||
DLog("error: \(err)")
|
||||
}
|
||||
if error == nil {
|
||||
if let existingPeripheral = scannedPeripherals[peripheral.identifier] {
|
||||
var scannedEncounter = existingPeripheral.encounter
|
||||
scannedEncounter.rssi = RSSI.doubleValue
|
||||
scannedPeripherals.updateValue((Date(), existingPeripheral.peripheral, scannedEncounter), forKey: peripheral.identifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
|
||||
DLog("Peripheral: \(peripheral) didModifyServices: \(invalidatedServices)")
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||
if let err = error {
|
||||
DLog("error: \(err)")
|
||||
}
|
||||
guard let service = peripheral.services?.first(where: { $0.uuid == BluetraceConfig.BluetoothServiceID }) else { return }
|
||||
peripheral.discoverCharacteristics([BluetraceConfig.BluetoothServiceID], for: service)
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||||
if let err = error {
|
||||
DLog("error: \(err)")
|
||||
}
|
||||
guard let characteristic = service.characteristics?.first(where: { $0.uuid == BluetraceConfig.BluetoothServiceID}) else { return }
|
||||
peripheral.readValue(for: characteristic)
|
||||
|
||||
// Do not need to wait for a successful read before writing, because no data from the read is needed in the write
|
||||
if let currEncounter = scannedPeripherals[peripheral.identifier] {
|
||||
EncounterMessageManager.shared.getTempId { (result) in
|
||||
guard let tempId = result else {
|
||||
DLog("broadcast msg not present")
|
||||
return
|
||||
}
|
||||
guard let rssi = currEncounter.encounter.rssi else {
|
||||
DLog("rssi should be present in \(currEncounter.encounter)")
|
||||
return
|
||||
}
|
||||
let encounterToBroadcast = EncounterBlob(modelC: DeviceIdentifier.getModel(),
|
||||
rssi: rssi,
|
||||
txPower: currEncounter.encounter.txPower,
|
||||
modelP: nil,
|
||||
msg: tempId,
|
||||
timestamp: Date().timeIntervalSince1970)
|
||||
|
||||
|
||||
do {
|
||||
let jsonMsg = try JSONEncoder().encode(encounterToBroadcast)
|
||||
let encryptedMsg = try Crypto.encrypt(dataToEncrypt: jsonMsg)
|
||||
let dataToWrite = CentralWriteData(modelC: BluetraceConfig.DummyModel,
|
||||
rssi: Double(BluetraceConfig.DummyRSSI),
|
||||
txPower: Double(BluetraceConfig.DummyTxPower),
|
||||
msg: encryptedMsg,
|
||||
org: BluetraceConfig.OrgID,
|
||||
v: BluetraceConfig.ProtocolVersion)
|
||||
let encodedData = try JSONEncoder().encode(dataToWrite)
|
||||
peripheral.writeValue(encodedData, for: characteristic, type: .withResponse)
|
||||
} catch {
|
||||
DLog("Error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
let debugLogs = ["characteristic": characteristic as AnyObject,
|
||||
"encounter": scannedPeripherals[peripheral.identifier] as AnyObject] as AnyObject
|
||||
|
||||
DLog("\(debugLogs)")
|
||||
guard error == nil else {
|
||||
DLog("Error: \(String(describing: error))")
|
||||
return
|
||||
}
|
||||
|
||||
if let scannedPeri = scannedPeripherals[peripheral.identifier],
|
||||
let characteristicValue = characteristic.value,
|
||||
shouldRecordEncounter(scannedPeri.encounter) {
|
||||
do {
|
||||
let peripheralCharData = try JSONDecoder().decode(PeripheralCharacteristicsData.self, from: characteristicValue)
|
||||
var encounterStruct = scannedPeri.encounter
|
||||
encounterStruct.msg = peripheralCharData.msg
|
||||
encounterStruct.update(modelP: peripheralCharData.modelP)
|
||||
encounterStruct.org = peripheralCharData.org
|
||||
encounterStruct.v = peripheralCharData.v
|
||||
encounterStruct.timestamp = Date()
|
||||
scannedPeripherals.updateValue((Date(), scannedPeri.peripheral, encounterStruct), forKey: peripheral.identifier)
|
||||
// here the remote blob will be msg and modelp if v1, msg if v2
|
||||
// local blob will be rssi, txpower, modelc
|
||||
try encounterStruct.saveRemotePeripheralToCoreData()
|
||||
DLog("Central recorded encounter with \(String(describing: scannedPeri.peripheral.name))")
|
||||
} catch {
|
||||
DLog("Error: \(error). CharacteristicValue is \(String(data: characteristicValue, encoding: .utf8) ?? "<nil>")")
|
||||
}
|
||||
} else {
|
||||
DLog("Error: scannedPeripherals[peripheral.identifier] is \(String(describing: scannedPeripherals[peripheral.identifier])), characteristic.value is \(String(describing: characteristic.value))")
|
||||
}
|
||||
|
||||
// regularly cleanup and close pending connections
|
||||
if (abs(lastCleanedScannedPeripherals.timeIntervalSince(Date())) > BluetraceConfig.CentralScanInterval) {
|
||||
cleanupScannedPeripherals()
|
||||
}
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
DLog("didWriteValueFor to peripheral: \(peripheral), for characteristics: \(characteristic). \(error != nil ? "error: \(error.debugDescription)" : "" )")
|
||||
central?.cancelPeripheralConnection(peripheral)
|
||||
}
|
||||
}
|
|
@ -1,10 +1,34 @@
|
|||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
struct CentralWriteData: Codable {
|
||||
var modelC: String // phone model of central
|
||||
var rssi: Double
|
||||
var txPower: Double?
|
||||
var msg: String // tempID
|
||||
var org: String
|
||||
var v: Int
|
||||
}
|
||||
|
||||
public struct PeripheralCharacteristicsData: Codable {
|
||||
var modelP: String // phone model of peripheral
|
||||
var msg: String // tempID
|
||||
var org: String
|
||||
var v: Int
|
||||
}
|
||||
|
||||
class EncounterMessageManager {
|
||||
let userDefaultsTempIdKey = "BROADCAST_MSG"
|
||||
let userDefaultsAdvtKey = "ADVT_DATA"
|
||||
let userDefaultsAdvtExpiryKey = "ADVT_EXPIRY"
|
||||
|
||||
struct CachedPayload {
|
||||
var payload: Data,
|
||||
expiry: TimeInterval
|
||||
}
|
||||
|
||||
private var payloadLookaside = [UUID: CachedPayload]()
|
||||
|
||||
static let shared = EncounterMessageManager()
|
||||
|
||||
var tempId: String? {
|
||||
|
@ -75,6 +99,92 @@ class EncounterMessageManager {
|
|||
}
|
||||
}
|
||||
|
||||
fileprivate func cleanUpExpiredCachedPayloads() {
|
||||
for payloadKey in payloadLookaside.keys {
|
||||
let currentTime = Date().timeIntervalSince1970
|
||||
guard let payload = payloadLookaside[payloadKey], payload.expiry < currentTime else {
|
||||
continue
|
||||
}
|
||||
// if payload exists and expiry time is less than current time, remove.
|
||||
payloadLookaside.removeValue(forKey: payloadKey)
|
||||
}
|
||||
}
|
||||
|
||||
func getWritePayloadForCentral(device: BLEDevice, onComplete: @escaping (Data?) -> Void) {
|
||||
guard let rssi = device.rssi else {
|
||||
DLog("getWritePayloadForCentral failed, no rssi")
|
||||
onComplete(nil)
|
||||
return
|
||||
}
|
||||
guard device.legacyPayloadCharacteristic != nil else {
|
||||
DLog("getWritePayloadForCentral failed, no legacyPayloadCharacteristic")
|
||||
onComplete(nil)
|
||||
return
|
||||
}
|
||||
getTempId { (result) in
|
||||
guard let tempId = result else {
|
||||
DLog("getWritePayloadForCentral failed, no tempid")
|
||||
onComplete(nil)
|
||||
return
|
||||
}
|
||||
var txPower: Double? = nil
|
||||
if let bleTxPower = device.txPower {
|
||||
txPower = Double(bleTxPower)
|
||||
}
|
||||
|
||||
let encounterToBroadcast = EncounterBlob(modelC: DeviceIdentifier.getModel(),
|
||||
rssi: Double(rssi),
|
||||
txPower: txPower,
|
||||
modelP: nil,
|
||||
msg: tempId,
|
||||
timestamp: Date().timeIntervalSince1970)
|
||||
|
||||
|
||||
do {
|
||||
let jsonMsg = try JSONEncoder().encode(encounterToBroadcast)
|
||||
let encryptedMsg = try Crypto.encrypt(dataToEncrypt: jsonMsg)
|
||||
let dataToWrite = CentralWriteData(modelC: BluetraceConfig.DummyModel,
|
||||
rssi: Double(BluetraceConfig.DummyRSSI),
|
||||
txPower: Double(BluetraceConfig.DummyTxPower),
|
||||
msg: encryptedMsg,
|
||||
org: BluetraceConfig.OrgID,
|
||||
v: BluetraceConfig.ProtocolVersion)
|
||||
let encodedData = try JSONEncoder().encode(dataToWrite)
|
||||
onComplete(encodedData)
|
||||
} catch {
|
||||
DLog("Error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getAdvertisementPayload(identifier: UUID, offset: Int, onComplete: @escaping (Data?) -> Void) {
|
||||
cleanUpExpiredCachedPayloads()
|
||||
guard offset > 0 else {
|
||||
// new request coming in
|
||||
getAdvertisementPayload{ (payloadToAdvertise) in
|
||||
if let payload = payloadToAdvertise {
|
||||
self.payloadLookaside[identifier] = CachedPayload(payload: payload, expiry: Date().timeIntervalSince1970 + BluetraceConfig.PayloadExpiry);
|
||||
}
|
||||
onComplete(payloadToAdvertise)
|
||||
}
|
||||
return
|
||||
}
|
||||
guard let cachedPayload = self.payloadLookaside[identifier] else {
|
||||
// subsequent request but nothing cached
|
||||
onComplete(nil)
|
||||
return
|
||||
}
|
||||
onComplete(cachedPayload.payload)
|
||||
}
|
||||
|
||||
func getLastKnownAdvertisementPayload(identifier: UUID) -> Data? {
|
||||
guard let cachedPayload = self.payloadLookaside[identifier] else {
|
||||
return nil
|
||||
}
|
||||
return cachedPayload.payload
|
||||
}
|
||||
|
||||
// this will give herald the payload it's after
|
||||
// gets the anon tempid for broadcasting
|
||||
func getAdvertisementPayload(onComplete: @escaping (Data?) -> Void) {
|
||||
// check expiry date of payload
|
||||
|
@ -147,3 +257,36 @@ class EncounterMessageManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension EncounterMessageManager: PayloadDataSupplier {
|
||||
|
||||
func payload(_ timestamp: PayloadTimestamp) -> PayloadData {
|
||||
return advertisedPayload!
|
||||
}
|
||||
|
||||
func payload(_ identifier: UUID, offset: Int, onComplete: @escaping (PayloadData?) -> Void) -> Void {
|
||||
getAdvertisementPayload(identifier: identifier, offset: offset, onComplete: onComplete)
|
||||
}
|
||||
|
||||
func payload(_ data: Data) -> [PayloadData] {
|
||||
// We share only one payload at a time due to the length.
|
||||
// No need to split payloads based on length or delimiter.
|
||||
return [PayloadData(data)]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension PayloadData {
|
||||
var shortName: String {
|
||||
do {
|
||||
let decodedPayload = try JSONDecoder().decode(PeripheralCharacteristicsData.self, from: self)
|
||||
let message = decodedPayload.msg
|
||||
return String(message.suffix(25))
|
||||
} catch {
|
||||
let startIndex = count >= 3 ? 3 : 0
|
||||
let endIndex = count >= 3 ? count-3 : 0
|
||||
return String(subdata(in: startIndex..<endIndex).base64EncodedString().prefix(6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
337
CovidSafe/Herald/Sensor/BLE/BLEDatabase.swift
Normal file
337
CovidSafe/Herald/Sensor/BLE/BLEDatabase.swift
Normal file
|
@ -0,0 +1,337 @@
|
|||
//
|
||||
// BLEDatabase.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
/// Registry for collating fragments of information from asynchronous BLE operations.
|
||||
protocol BLEDatabase {
|
||||
|
||||
/// Add delegate for handling database events
|
||||
func add(delegate: BLEDatabaseDelegate)
|
||||
|
||||
/// Get or create device for collating information from asynchronous BLE operations.
|
||||
func device(_ identifier: TargetIdentifier) -> BLEDevice
|
||||
|
||||
/// Get or create device for collating information from asynchronous BLE operations.
|
||||
func device(_ peripheral: CBPeripheral, delegate: CBPeripheralDelegate) -> BLEDevice
|
||||
|
||||
/// Get or create device for collating information from asynchronous BLE operations.
|
||||
func device(_ payload: PayloadData) -> BLEDevice
|
||||
|
||||
/// Get if a device exists
|
||||
func hasDevice(_ payload: PayloadData) -> Bool
|
||||
|
||||
/// Get all devices
|
||||
func devices() -> [BLEDevice]
|
||||
|
||||
/// Delete device from database
|
||||
func delete(_ identifier: TargetIdentifier)
|
||||
}
|
||||
|
||||
/// Delegate for receiving registry create/update/delete events
|
||||
protocol BLEDatabaseDelegate {
|
||||
|
||||
func bleDatabase(didCreate device: BLEDevice)
|
||||
|
||||
func bleDatabase(didUpdate device: BLEDevice, attribute: BLEDeviceAttribute)
|
||||
|
||||
func bleDatabase(didDelete device: BLEDevice)
|
||||
}
|
||||
|
||||
extension BLEDatabaseDelegate {
|
||||
func bleDatabase(didCreate device: BLEDevice) {}
|
||||
|
||||
func bleDatabase(didUpdate device: BLEDevice, attribute: BLEDeviceAttribute) {}
|
||||
|
||||
func bleDatabase(didDelete device: BLEDevice) {}
|
||||
}
|
||||
|
||||
class ConcreteBLEDatabase : NSObject, BLEDatabase, BLEDeviceDelegate {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "BLE.ConcreteBLEDatabase")
|
||||
private var delegates: [BLEDatabaseDelegate] = []
|
||||
private var database: [TargetIdentifier : BLEDevice] = [:]
|
||||
private var queue = DispatchQueue(label: "Sensor.BLE.ConcreteBLEDatabase")
|
||||
|
||||
func add(delegate: BLEDatabaseDelegate) {
|
||||
delegates.append(delegate)
|
||||
}
|
||||
|
||||
func devices() -> [BLEDevice] {
|
||||
return database.values.map { $0 }
|
||||
}
|
||||
|
||||
func device(_ identifier: TargetIdentifier) -> BLEDevice {
|
||||
if database[identifier] == nil {
|
||||
let device = BLEDevice(identifier, delegate: self)
|
||||
database[identifier] = device
|
||||
queue.async {
|
||||
self.logger.debug("create (device=\(identifier))")
|
||||
self.delegates.forEach { $0.bleDatabase(didCreate: device) }
|
||||
}
|
||||
}
|
||||
let device = database[identifier]!
|
||||
return device
|
||||
}
|
||||
|
||||
func device(_ peripheral: CBPeripheral, delegate: CBPeripheralDelegate) -> BLEDevice {
|
||||
let identifier = TargetIdentifier(peripheral: peripheral)
|
||||
if database[identifier] == nil {
|
||||
let device = BLEDevice(identifier, delegate: self)
|
||||
database[identifier] = device
|
||||
queue.async {
|
||||
self.logger.debug("create (device=\(identifier))")
|
||||
self.delegates.forEach { $0.bleDatabase(didCreate: device) }
|
||||
}
|
||||
}
|
||||
let device = database[identifier]!
|
||||
if device.peripheral != peripheral {
|
||||
device.peripheral = peripheral
|
||||
peripheral.delegate = delegate
|
||||
}
|
||||
return device
|
||||
}
|
||||
|
||||
func device(_ payload: PayloadData) -> BLEDevice {
|
||||
if let device = database.values.filter({ $0.payloadData == payload }).first {
|
||||
return device
|
||||
}
|
||||
// Create temporary UUID, the taskRemoveDuplicatePeripherals function
|
||||
// will delete this when a direct connection to the peripheral has been
|
||||
// established
|
||||
let identifier = TargetIdentifier(UUID().uuidString)
|
||||
let placeholder = device(identifier)
|
||||
placeholder.payloadData = payload
|
||||
return placeholder
|
||||
}
|
||||
|
||||
func hasDevice(_ payload: PayloadData) -> Bool {
|
||||
if database.values.filter({ $0.payloadData == payload }).first != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func delete(_ identifier: TargetIdentifier) {
|
||||
guard let device = database[identifier] else {
|
||||
return
|
||||
}
|
||||
database[identifier] = nil
|
||||
queue.async {
|
||||
self.logger.debug("delete (device=\(identifier))")
|
||||
self.delegates.forEach { $0.bleDatabase(didDelete: device) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:- BLEDeviceDelegate
|
||||
|
||||
func device(_ device: BLEDevice, didUpdate attribute: BLEDeviceAttribute) {
|
||||
queue.async {
|
||||
self.logger.debug("update (device=\(device.identifier),attribute=\(attribute.rawValue))")
|
||||
self.delegates.forEach { $0.bleDatabase(didUpdate: device, attribute: attribute) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:- BLEDatabase data
|
||||
|
||||
public class BLEDevice : NSObject {
|
||||
/// Device registratiion timestamp
|
||||
let createdAt: Date
|
||||
/// Last time anything changed, e.g. attribute update
|
||||
var lastUpdatedAt: Date
|
||||
/// Last time a wake up call was received from this device (iOS only)
|
||||
var lastNotifiedAt: Date = Date.distantPast
|
||||
/// Ephemeral device identifier, e.g. peripheral identifier UUID
|
||||
let identifier: TargetIdentifier
|
||||
/// Pseudo device address for tracking devices that change device identifier constantly like the Samsung A10, A20 and Note 8
|
||||
var pseudoDeviceAddress: BLEPseudoDeviceAddress? {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
}}
|
||||
/// Delegate for listening to attribute updates events.
|
||||
let delegate: BLEDeviceDelegate
|
||||
/// CoreBluetooth peripheral object for interacting with this device.
|
||||
var peripheral: CBPeripheral? {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
delegate.device(self, didUpdate: .peripheral)
|
||||
}}
|
||||
/// Service characteristic for signalling between BLE devices, e.g. to keep awake
|
||||
var signalCharacteristic: CBCharacteristic? {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
delegate.device(self, didUpdate: .signalCharacteristic)
|
||||
}}
|
||||
/// Service characteristic for reading payload data
|
||||
var payloadCharacteristic: CBCharacteristic? {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
delegate.device(self, didUpdate: .payloadCharacteristic)
|
||||
}}
|
||||
var legacyPayloadCharacteristic: CBCharacteristic? {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
delegate.device(self, didUpdate: .payloadCharacteristic)
|
||||
}}
|
||||
/// Device operating system, this is necessary for selecting different interaction procedures for each platform.
|
||||
var operatingSystem: BLEDeviceOperatingSystem = .unknown {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
delegate.device(self, didUpdate: .operatingSystem)
|
||||
}}
|
||||
/// Device is receive only, this is necessary for filtering payload sharing data
|
||||
var receiveOnly: Bool = false {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
}}
|
||||
/// Payload data acquired from the device via payloadCharacteristic read
|
||||
var payloadData: PayloadData? {
|
||||
didSet {
|
||||
payloadDataLastUpdatedAt = Date()
|
||||
lastUpdatedAt = payloadDataLastUpdatedAt
|
||||
delegate.device(self, didUpdate: .payloadData)
|
||||
}}
|
||||
/// Payload data last update timestamp, this is used to determine what needs to be shared with peers.
|
||||
var payloadDataLastUpdatedAt: Date = Date.distantPast
|
||||
/// Payload data already shared with this peer
|
||||
var payloadSharingData: [PayloadData] = []
|
||||
/// Most recent RSSI measurement taken by readRSSI or didDiscover.
|
||||
var rssi: BLE_RSSI? {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
rssiLastUpdatedAt = lastUpdatedAt
|
||||
delegate.device(self, didUpdate: .rssi)
|
||||
}}
|
||||
/// RSSI last update timestamp, this is used to track last advertised at without relying on didDiscover
|
||||
var rssiLastUpdatedAt: Date = Date.distantPast
|
||||
/// Transmit power data where available (only provided by Android devices)
|
||||
var txPower: BLE_TxPower? {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
delegate.device(self, didUpdate: .txPower)
|
||||
}}
|
||||
/// Track discovered at timestamp, used by taskConnect to prioritise connection when device runs out of concurrent connection capacity
|
||||
var lastDiscoveredAt: Date = Date.distantPast
|
||||
/// Track connect request at timestamp, used by taskConnect to prioritise connection when device runs out of concurrent connection capacity
|
||||
var lastConnectRequestedAt: Date = Date.distantPast
|
||||
/// Track connected at timestamp, used by taskConnect to prioritise connection when device runs out of concurrent connection capacity
|
||||
var lastConnectedAt: Date? {
|
||||
didSet {
|
||||
// Reset lastDisconnectedAt
|
||||
lastDisconnectedAt = nil
|
||||
// Reset lastConnectionInitiationAttempt
|
||||
lastConnectionInitiationAttempt = nil
|
||||
}}
|
||||
/// Track read payload request at timestamp, used by readPayload to de-duplicate requests from asynchronous calls
|
||||
var lastReadPayloadRequestedAt: Date = Date.distantPast
|
||||
/// Track Herald initiated connection attempts - workaround for iOS peripheral caching incorrect state bug
|
||||
var lastConnectionInitiationAttempt: Date?
|
||||
/// Track disconnected at timestamp, used by taskConnect to prioritise connection when device runs out of concurrent connection capacity
|
||||
var lastDisconnectedAt: Date? {
|
||||
didSet {
|
||||
// Reset lastConnectionInitiationAttempt
|
||||
lastConnectionInitiationAttempt = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Last advert timestamp, inferred from payloadDataLastUpdatedAt, payloadSharingDataLastUpdatedAt, rssiLastUpdatedAt
|
||||
var lastAdvertAt: Date { get {
|
||||
max(createdAt, lastDiscoveredAt, payloadDataLastUpdatedAt, rssiLastUpdatedAt)
|
||||
}}
|
||||
|
||||
/// Time interval since last payload data update, this is used to identify devices that require a payload update.
|
||||
var timeIntervalSinceLastPayloadDataUpdate: TimeInterval { get {
|
||||
Date().timeIntervalSince(payloadDataLastUpdatedAt)
|
||||
}}
|
||||
|
||||
/// Time interval since created at timestamp
|
||||
var timeIntervalSinceCreated: TimeInterval { get {
|
||||
Date().timeIntervalSince(createdAt)
|
||||
}}
|
||||
/// Time interval since last attribute value update, this is used to identify devices that may have expired and should be removed from the database.
|
||||
var timeIntervalSinceLastUpdate: TimeInterval { get {
|
||||
Date().timeIntervalSince(lastUpdatedAt)
|
||||
}}
|
||||
/// Time interval since last advert detected, this is used to detect concurrent connection quota and prioritise disconnections
|
||||
var timeIntervalSinceLastAdvert: TimeInterval { get {
|
||||
Date().timeIntervalSince(lastAdvertAt)
|
||||
}}
|
||||
/// Time interval between last connection request, this is used to priortise disconnections
|
||||
var timeIntervalSinceLastConnectRequestedAt: TimeInterval { get {
|
||||
Date().timeIntervalSince(lastConnectRequestedAt)
|
||||
}}
|
||||
/// Time interval between last connected at and last advert, this is used to estimate last period of continuous tracking, to priortise disconnections
|
||||
var timeIntervalSinceLastDisconnectedAt: TimeInterval { get {
|
||||
guard let lastDisconnectedAt = lastDisconnectedAt else {
|
||||
return Date().timeIntervalSince(createdAt)
|
||||
}
|
||||
return Date().timeIntervalSince(lastDisconnectedAt)
|
||||
}}
|
||||
/// Time interval between last connected at and last advert, this is used to estimate last period of continuous tracking, to priortise disconnections
|
||||
var timeIntervalBetweenLastConnectedAndLastAdvert: TimeInterval { get {
|
||||
guard let lastConnectedAt = lastConnectedAt, lastAdvertAt > lastConnectedAt else {
|
||||
return TimeInterval(0)
|
||||
}
|
||||
return lastAdvertAt.timeIntervalSince(lastConnectedAt)
|
||||
}}
|
||||
|
||||
public override var description: String { get {
|
||||
return "BLEDevice[id=\(identifier),os=\(operatingSystem.rawValue),payload=\(payloadData?.shortName ?? "nil"),address=\(pseudoDeviceAddress?.data.base64EncodedString() ?? "nil")]"
|
||||
}}
|
||||
|
||||
init(_ identifier: TargetIdentifier, delegate: BLEDeviceDelegate) {
|
||||
self.createdAt = Date()
|
||||
self.identifier = identifier
|
||||
self.delegate = delegate
|
||||
lastUpdatedAt = createdAt
|
||||
}
|
||||
}
|
||||
|
||||
protocol BLEDeviceDelegate {
|
||||
func device(_ device: BLEDevice, didUpdate attribute: BLEDeviceAttribute)
|
||||
}
|
||||
|
||||
enum BLEDeviceAttribute : String {
|
||||
case peripheral, signalCharacteristic, payloadCharacteristic, payloadSharingCharacteristic, operatingSystem, payloadData, rssi, txPower
|
||||
}
|
||||
|
||||
enum BLEDeviceOperatingSystem : String {
|
||||
case android, ios, restored, unknown, shared
|
||||
}
|
||||
|
||||
/// RSSI in dBm.
|
||||
typealias BLE_RSSI = Int
|
||||
|
||||
typealias BLE_TxPower = Int
|
||||
|
||||
class BLEPseudoDeviceAddress {
|
||||
let address: Int
|
||||
let data: Data
|
||||
var description: String { get {
|
||||
return "BLEPseudoDeviceAddress(address=\(address),data=\(data.base64EncodedString()))"
|
||||
}}
|
||||
|
||||
init?(fromAdvertisementData: [String: Any]) {
|
||||
guard let manufacturerData = fromAdvertisementData["kCBAdvDataManufacturerData"] as? Data else {
|
||||
return nil
|
||||
}
|
||||
guard let manufacturerId = manufacturerData.uint16(0), manufacturerId == BLESensorConfiguration.manufacturerIdForSensor else {
|
||||
return nil
|
||||
}
|
||||
guard manufacturerData.count == 8 else {
|
||||
return nil
|
||||
}
|
||||
data = Data(manufacturerData.subdata(in: 2..<8))
|
||||
var longValueData = Data(repeating: 0, count: 2)
|
||||
longValueData.append(data)
|
||||
guard let longValue = longValueData.int64(0) else {
|
||||
return nil
|
||||
}
|
||||
address = Int(longValue)
|
||||
}
|
||||
}
|
997
CovidSafe/Herald/Sensor/BLE/BLEReceiver.swift
Normal file
997
CovidSafe/Herald/Sensor/BLE/BLEReceiver.swift
Normal file
|
@ -0,0 +1,997 @@
|
|||
//
|
||||
// BLEReceiver.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
import os
|
||||
|
||||
/**
|
||||
Beacon receiver scans for peripherals with fixed service UUID.
|
||||
*/
|
||||
protocol BLEReceiver : Sensor {
|
||||
}
|
||||
|
||||
/**
|
||||
Beacon receiver scans for peripherals with fixed service UUID in foreground and background modes. Background scan
|
||||
for Android is trivial as scanForPeripherals will always return all Android devices on every call. Background scan for iOS
|
||||
devices that are transmitting in background mode is more complex, requiring an open connection to subscribe to a
|
||||
notifying characteristic that is used as trigger for keeping both iOS devices in background state (rather than suspended
|
||||
or killed). For iOS - iOS devices, on detection, the receiver will (1) write blank data to the transmitter, which triggers the
|
||||
transmitter to send a characteristic data update after 8 seconds, which in turns (2) triggers the receiver to receive a value
|
||||
update notification, to (3) create the opportunity for a read RSSI call and repeat of this looped process that keeps both
|
||||
devices awake.
|
||||
|
||||
Please note, the iOS - iOS process is unreliable if (1) the user switches off bluetooth via Airplane mode settings, (2) the
|
||||
device reboots, and (3) it will fail completely if the app has been killed by the user. These are conditions that cannot be
|
||||
handled reliably by CoreBluetooth state restoration.
|
||||
*/
|
||||
class ConcreteBLEReceiver: NSObject, BLEReceiver, BLEDatabaseDelegate, CBCentralManagerDelegate, CBPeripheralDelegate {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "BLE.ConcreteBLEReceiver")
|
||||
private var delegates: [SensorDelegate] = []
|
||||
private var connectionDelegate: SensorDelegate?
|
||||
/// Dedicated sequential queue for all beacon transmitter and receiver tasks.
|
||||
private let queue: DispatchQueue!
|
||||
private let delegateQueue: DispatchQueue
|
||||
/// Database of peripherals
|
||||
private let database: BLEDatabase
|
||||
/// Payload data supplier for parsing shared payloads
|
||||
private let payloadDataSupplier: PayloadDataSupplier
|
||||
/// Central manager for managing all connections, using a single manager for simplicity.
|
||||
private var central: CBCentralManager!
|
||||
/// Dummy data for writing to the transmitter to trigger state restoration or resume from suspend state to background state.
|
||||
private let emptyData = Data(repeating: 0, count: 0)
|
||||
/**
|
||||
Shifting timer for triggering peripheral scan just before the app switches from background to suspend state following a
|
||||
call to CoreBluetooth delegate methods. Apple documentation suggests the time limit is about 10 seconds.
|
||||
*/
|
||||
private var scanTimer: DispatchSourceTimer?
|
||||
/// Dedicated sequential queue for the shifting timer.
|
||||
private let scanTimerQueue = DispatchQueue(label: "Sensor.BLE.ConcreteBLEReceiver.ScanTimer")
|
||||
/// Dedicated sequential queue for the actual scan call.
|
||||
private let scheduleScanQueue = DispatchQueue(label: "Sensor.BLE.ConcreteBLEReceiver.ScheduleScan")
|
||||
/// Track scan interval and up time statistics for the receiver, for debug purposes.
|
||||
private let statistics = TimeIntervalSample()
|
||||
/// Scan result queue for recording discovered devices with no immediate pending action.
|
||||
private var scanResults: [BLEDevice] = []
|
||||
|
||||
/// Create a BLE receiver that shares the same sequential dispatch queue as the transmitter because concurrent transmit and receive
|
||||
/// operations impacts CoreBluetooth stability. The receiver and transmitter share a common database of devices to enable the transmitter
|
||||
/// to register centrals for resolution by the receiver as peripherals to create symmetric connections. The payload data supplier provides
|
||||
/// the actual payload data to be transmitted and received via BLE.
|
||||
required init(queue: DispatchQueue, delegateQueue: DispatchQueue, database: BLEDatabase, payloadDataSupplier: PayloadDataSupplier) {
|
||||
self.queue = queue
|
||||
self.delegateQueue = delegateQueue
|
||||
self.database = database
|
||||
self.payloadDataSupplier = payloadDataSupplier
|
||||
super.init()
|
||||
database.add(delegate: self)
|
||||
}
|
||||
|
||||
func add(delegate: SensorDelegate) {
|
||||
delegates.append(delegate)
|
||||
}
|
||||
|
||||
func addConnectionDelegate(delegate: SensorDelegate) {
|
||||
connectionDelegate = delegate
|
||||
}
|
||||
|
||||
func removeConnectionDelegate() {
|
||||
connectionDelegate = nil
|
||||
}
|
||||
|
||||
func start() {
|
||||
logger.debug("start")
|
||||
|
||||
if central == nil {
|
||||
self.central = CBCentralManager(delegate: self, queue: queue, options: [
|
||||
CBCentralManagerOptionRestoreIdentifierKey : "Sensor.BLE.ConcreteBLEReceiver",
|
||||
// Set this to false to stop iOS from displaying an alert if the app is opened while bluetooth is off.
|
||||
CBCentralManagerOptionShowPowerAlertKey : false])
|
||||
}
|
||||
// Start scanning
|
||||
if central.state == .poweredOn {
|
||||
scan("start")
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
logger.debug("stop")
|
||||
guard central != nil else {
|
||||
return
|
||||
}
|
||||
guard central.isScanning else {
|
||||
logger.fault("stop denied, already stopped")
|
||||
central = nil
|
||||
return
|
||||
}
|
||||
// Stop scanning
|
||||
scanTimer?.cancel()
|
||||
scanTimer = nil
|
||||
queue.async {
|
||||
self.central.stopScan()
|
||||
self.central = nil
|
||||
}
|
||||
// Cancel all connections, the resulting didDisconnect and didFailToConnect
|
||||
database.devices().forEach() { device in
|
||||
if let peripheral = device.peripheral, peripheral.state != .disconnected {
|
||||
disconnect("stop", peripheral)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:- Scan for peripherals and initiate connection if required
|
||||
|
||||
/// All work starts from scan loop.
|
||||
func scan(_ source: String) {
|
||||
statistics.add()
|
||||
logger.debug("scan (source=\(source),statistics={\(statistics.description)})")
|
||||
guard central.state == .poweredOn else {
|
||||
logger.fault("scan failed, bluetooth is not powered on")
|
||||
return
|
||||
}
|
||||
// Scan for periperals advertising the sensor service.
|
||||
// This will find all Android and iOS foreground adverts
|
||||
// but it will miss the iOS background adverts unless
|
||||
// location has been enabled and screen is on for a moment.
|
||||
queue.async { self.taskScanForPeripherals() }
|
||||
// Register connected peripherals that are advertising the
|
||||
// sensor service. This catches the orphan peripherals that
|
||||
// may have been missed by CoreBluetooth during state
|
||||
// restoration or internal errors.
|
||||
queue.async { self.taskRegisterConnectedPeripherals() }
|
||||
// Resolve peripherals by device identifier obtained via
|
||||
// the transmitter. When an iOS central connects to this
|
||||
// peripheral, the transmitter code registers the central's
|
||||
// address as a new device pending resolution here to
|
||||
// establish a symmetric connection. This enables either
|
||||
// device to detect the other (e.g. with screen on)
|
||||
// and triggering both devices to detect each other.
|
||||
queue.async { self.taskResolveDevicePeripherals() }
|
||||
// Remove devices that have not been seen for a while as
|
||||
// the identifier would have changed after about 20 mins,
|
||||
// thus it is wasteful to maintain a reference.
|
||||
queue.async { self.taskRemoveExpiredDevices() }
|
||||
// Remove duplicate devices with the same payload but
|
||||
// different identifiers. This happens frequently as
|
||||
// device address changes at regular intervals as part
|
||||
// of the Bluetooth privacy feature, thus it looks like
|
||||
// a new device but is actually associated with the same
|
||||
// payload. All references to the duplicate will be
|
||||
// removed but the actual connection will be terminated
|
||||
// by CoreBluetooth, often showing an API misuse warning
|
||||
// which can be ignored.
|
||||
queue.async { self.taskRemoveDuplicatePeripherals() }
|
||||
// iOS devices are kept in background state indefinitely
|
||||
// (instead of dropping into suspended or terminated state)
|
||||
// by a series of time delayed BLE operations. While this
|
||||
// device is awake, it will write data to other iOS devices
|
||||
// to keep them awake, and vice versa.
|
||||
queue.async { self.taskWakeTransmitters() }
|
||||
// All devices have an upper limit on the number of concurrent
|
||||
// BLE connections it can maintain. For iOS, it is usually 12
|
||||
// or above. iOS devices maintain an active connection with
|
||||
// other iOS devices to keep awake and obtain regular RSSI
|
||||
// measurements, thus it can track up to 12 iOS devices at any
|
||||
// moment in time. Above this figure, this device will need
|
||||
// to rotate (disconnect/connect) connections to multiplex
|
||||
// between the iOS devices for coverage. This is unnecessary
|
||||
// for tracking Android devices as they are tracked by scan
|
||||
// only. A connection to Android is only required for reading
|
||||
// its payload upon discovery.
|
||||
queue.async { self.taskIosMultiplex() }
|
||||
// Connect to discovered devices if the device has pending tasks.
|
||||
// The vast majority of devices will be connected immediately upon
|
||||
// discovery, if they have a pending task (e.g. to establish its
|
||||
// operating system or read its payload). Devices may be discovered
|
||||
// but not have a pending task if they have already been fully
|
||||
// resolved (e.g. has operating system, payload and recent RSSI
|
||||
// measuremnet), these are placed in the scan results queue for
|
||||
// regular checking by this connect task (e.g. to read RSSI if
|
||||
// the existing value is now out of date).
|
||||
queue.async { self.taskConnect() }
|
||||
// Schedule this scan call again for execution in at least 8 seconds
|
||||
// time to repeat the scan loop. The actual call may be delayed beyond
|
||||
// the 8 second delay from this point because all terminating operations
|
||||
// (i.e. events that will eventually lead the app to enter suspended
|
||||
// state if nothing else happens) calls this function to keep the loop
|
||||
// running indefinitely. The 8 or less seconds delay was chosen to
|
||||
// ensure the scan call is activated before the app naturally enters
|
||||
// suspended state, but not so soon the loop runs too often.
|
||||
scheduleScan("scan")
|
||||
|
||||
// Make a call to the messages API if needed.
|
||||
// Dispatch in background queue
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
MessageAPI.getMessagesIfNeeded() { (messageResponse, error) in
|
||||
if let error = error {
|
||||
DLog("Get messages error: \(error.localizedDescription)")
|
||||
}
|
||||
// We currently dont do anything with the response. Messages are delivered via APN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Schedule scan for beacons after a delay of 8 seconds to start scan again just before
|
||||
state change from background to suspended. Scan is sufficient for finding Android
|
||||
devices repeatedly in both foreground and background states.
|
||||
*/
|
||||
private func scheduleScan(_ source: String) {
|
||||
scheduleScanQueue.sync {
|
||||
scanTimer?.cancel()
|
||||
scanTimer = DispatchSource.makeTimerSource(queue: scanTimerQueue)
|
||||
scanTimer?.schedule(deadline: DispatchTime.now() + BLESensorConfiguration.notificationDelay)
|
||||
scanTimer?.setEventHandler { [weak self] in
|
||||
self?.scan("scheduleScan|"+source)
|
||||
}
|
||||
scanTimer?.resume()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Scan for peripherals advertising the beacon service.
|
||||
*/
|
||||
private func taskScanForPeripherals() {
|
||||
// Scan for peripherals -> didDiscover
|
||||
central.scanForPeripherals(
|
||||
withServices: [BLESensorConfiguration.serviceUUID],
|
||||
options: [CBCentralManagerScanOptionSolicitedServiceUUIDsKey: [BLESensorConfiguration.serviceUUID]])
|
||||
}
|
||||
|
||||
/**
|
||||
Register all connected peripherals advertising the sensor service as a device.
|
||||
*/
|
||||
private func taskRegisterConnectedPeripherals() {
|
||||
central.retrieveConnectedPeripherals(withServices: [BLESensorConfiguration.serviceUUID]).forEach() { peripheral in
|
||||
let targetIdentifier = TargetIdentifier(peripheral: peripheral)
|
||||
let device = database.device(targetIdentifier)
|
||||
if device.peripheral == nil || device.peripheral != peripheral {
|
||||
logger.debug("taskRegisterConnectedPeripherals (device=\(device))")
|
||||
_ = database.device(peripheral, delegate: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Resolve peripheral for all database devices. This enables the symmetric connection feature where connections from central to peripheral (BLETransmitter) registers the existence
|
||||
of a potential peripheral for resolution by this central (BLEReceiver).
|
||||
*/
|
||||
private func taskResolveDevicePeripherals() {
|
||||
let devicesToResolve = database.devices().filter { $0.peripheral == nil }
|
||||
devicesToResolve.forEach() { device in
|
||||
guard let identifier = UUID(uuidString: device.identifier) else {
|
||||
return
|
||||
}
|
||||
let peripherals = central.retrievePeripherals(withIdentifiers: [identifier])
|
||||
if let peripheral = peripherals.last {
|
||||
logger.debug("taskResolveDevicePeripherals (resolved=\(device))")
|
||||
_ = database.device(peripheral, delegate: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Remove devices that have not been updated for over an hour, as the UUID is likely to have changed after being out of range for over 20 minutes, so it will require discovery.
|
||||
*/
|
||||
private func taskRemoveExpiredDevices() {
|
||||
let devicesToRemove = database.devices().filter { Date().timeIntervalSince($0.lastUpdatedAt) > BluetraceConfig.PeripheralCleanInterval }
|
||||
devicesToRemove.forEach() { device in
|
||||
logger.debug("taskRemoveExpiredDevices (remove=\(device))")
|
||||
database.delete(device.identifier)
|
||||
if let peripheral = device.peripheral {
|
||||
disconnect("taskRemoveExpiredDevices", peripheral)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Remove devices with the same payload data but different peripherals.
|
||||
*/
|
||||
private func taskRemoveDuplicatePeripherals() {
|
||||
var index: [PayloadData:BLEDevice] = [:]
|
||||
let devices = database.devices()
|
||||
devices.forEach() { device in
|
||||
guard let payloadData = device.payloadData else {
|
||||
return
|
||||
}
|
||||
guard let duplicate = index[payloadData] else {
|
||||
index[payloadData] = device
|
||||
return
|
||||
}
|
||||
var keeping = device
|
||||
if device.peripheral != nil, duplicate.peripheral == nil {
|
||||
keeping = device
|
||||
} else if duplicate.peripheral != nil, device.peripheral == nil {
|
||||
keeping = duplicate
|
||||
} else if device.payloadDataLastUpdatedAt > duplicate.payloadDataLastUpdatedAt {
|
||||
keeping = device
|
||||
} else {
|
||||
keeping = duplicate
|
||||
}
|
||||
let discarding = (keeping.identifier == device.identifier ? duplicate : device)
|
||||
index[payloadData] = keeping
|
||||
database.delete(discarding.identifier)
|
||||
self.logger.debug("taskRemoveDuplicatePeripherals (payload=\(payloadData.shortName),device=\(device.identifier),duplicate=\(duplicate.identifier),keeping=\(keeping.identifier))")
|
||||
// CoreBluetooth will eventually give warning and disconnect actual duplicate silently.
|
||||
// While calling disconnect here is cleaner but it will trigger didDiscover and
|
||||
// retain the duplicates. Expect to see message :
|
||||
// [CoreBluetooth] API MISUSE: Forcing disconnection of unused peripheral
|
||||
// <CBPeripheral: XXX, identifier = XXX, name = iPhone, state = connected>.
|
||||
// Did you forget to cancel the connection?
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Wake transmitter on all connected iOS devices
|
||||
*/
|
||||
private func taskWakeTransmitters() {
|
||||
database.devices().forEach() { device in
|
||||
guard device.operatingSystem == .ios, let peripheral = device.peripheral, peripheral.state == .connected else {
|
||||
return
|
||||
}
|
||||
guard device.timeIntervalSinceLastUpdate < TimeInterval.minute else {
|
||||
// Throttle back keep awake calls when out of range, issue pending connect instead
|
||||
connect("taskWakeTransmitters", peripheral)
|
||||
return
|
||||
}
|
||||
wakeTransmitter("taskWakeTransmitters", device)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Connect to devices and maintain concurrent connection quota
|
||||
*/
|
||||
private func taskConnect() {
|
||||
// Get recently discovered devices
|
||||
let didDiscover = taskConnectScanResults()
|
||||
// Identify recently discovered devices with pending tasks : connect -> nextTask
|
||||
let hasPendingTask = didDiscover.filter({ deviceHasPendingTask($0) })
|
||||
// Identify all connected (iOS) devices to trigger refresh : connect -> nextTask
|
||||
let toBeRefreshed = database.devices().filter({ !hasPendingTask.contains($0) && $0.peripheral?.state == .connected })
|
||||
// Identify all unconnected devices with unknown operating system, these are
|
||||
// created by ConcreteBLETransmitter on characteristic write, to ensure all
|
||||
// centrals that connect to this peripheral are recorded, to enable this central
|
||||
// to attempt connection to the peripheral, thus establishing a bi-directional
|
||||
// connection. This is essential for iOS-iOS background detection, where the
|
||||
// discovery of phoneB by phoneA, and a connection from A to B, will trigger
|
||||
// B to connect to A, thus assuming location permission has been enabled, it
|
||||
// will only require screen ON at either phone to trigger bi-directional connection.
|
||||
let asymmetric = database.devices().filter({ !hasPendingTask.contains($0)
|
||||
&& $0.operatingSystem == .unknown
|
||||
&& $0.timeIntervalSinceLastUpdate < TimeInterval.minute
|
||||
&& $0.peripheral?.state != .connected })
|
||||
// Connect to recently discovered devices with pending tasks
|
||||
hasPendingTask.forEach() { device in
|
||||
guard let peripheral = device.peripheral else {
|
||||
return
|
||||
}
|
||||
connect("taskConnect|hasPending", peripheral);
|
||||
}
|
||||
// Refresh connection to existing devices to trigger next task
|
||||
toBeRefreshed.forEach() { device in
|
||||
guard let peripheral = device.peripheral else {
|
||||
return
|
||||
}
|
||||
connect("taskConnect|refresh", peripheral);
|
||||
}
|
||||
// Connect to unknown devices that have written to this peripheral
|
||||
asymmetric.forEach() { device in
|
||||
guard let peripheral = device.peripheral else {
|
||||
return
|
||||
}
|
||||
connect("taskConnect|asymmetric", peripheral);
|
||||
}
|
||||
}
|
||||
|
||||
/// Empty scan results to produce a list of recently discovered devices for connection and processing
|
||||
private func taskConnectScanResults() -> [BLEDevice] {
|
||||
var set: Set<BLEDevice> = []
|
||||
var list: [BLEDevice] = []
|
||||
while let device = scanResults.popLast() {
|
||||
if set.insert(device).inserted, let peripheral = device.peripheral, peripheral.state != .connected {
|
||||
list.append(device)
|
||||
logger.debug("taskConnectScanResults, didDiscover (device=\(device))")
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/// Check if device has pending task
|
||||
private func deviceHasPendingTask(_ device: BLEDevice) -> Bool {
|
||||
// Resolve operating system
|
||||
if device.operatingSystem == .unknown || device.operatingSystem == .restored {
|
||||
return true
|
||||
}
|
||||
// Read payload
|
||||
if device.payloadData == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Payload update
|
||||
if device.timeIntervalSinceLastPayloadDataUpdate > BluetraceConfig.PeripheralPayloadExpiry {
|
||||
return true
|
||||
}
|
||||
|
||||
// iOS should always be connected
|
||||
if device.operatingSystem == .ios, let peripheral = device.peripheral, peripheral.state != .connected {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Check if iOS device is waiting for connection and free capacity if required
|
||||
private func taskIosMultiplex() {
|
||||
// Identify iOS devices
|
||||
let devices = database.devices().filter({ $0.operatingSystem == .ios && $0.peripheral != nil })
|
||||
// Get a list of connected devices and uptime
|
||||
let connected = devices.filter({ $0.peripheral?.state == .connected }).sorted(by: { $0.timeIntervalBetweenLastConnectedAndLastAdvert > $1.timeIntervalBetweenLastConnectedAndLastAdvert })
|
||||
// Get a list of connecting devices
|
||||
let pending = devices.filter({ $0.peripheral?.state != .connected }).sorted(by: { $0.lastConnectRequestedAt < $1.lastConnectRequestedAt })
|
||||
logger.debug("taskIosMultiplex summary (connected=\(connected.count),pending=\(pending.count))")
|
||||
connected.forEach() { device in
|
||||
logger.debug("taskIosMultiplex, connected (device=\(device.description),upTime=\(device.timeIntervalBetweenLastConnectedAndLastAdvert))")
|
||||
}
|
||||
pending.forEach() { device in
|
||||
logger.debug("taskIosMultiplex, pending (device=\(device.description),downTime=\(device.timeIntervalSinceLastConnectRequestedAt))")
|
||||
}
|
||||
// Retry all pending connections if there is surplus capacity
|
||||
if connected.count < BLESensorConfiguration.concurrentConnectionQuota {
|
||||
pending.forEach() { device in
|
||||
guard let toBeConnected = device.peripheral else {
|
||||
return
|
||||
}
|
||||
connect("taskIosMultiplex|retry", toBeConnected);
|
||||
}
|
||||
}
|
||||
// Initiate multiplexing when capacity has been reached
|
||||
guard connected.count > BLESensorConfiguration.concurrentConnectionQuota, pending.count > 0, let deviceToBeDisconnected = connected.first, let peripheralToBeDisconnected = deviceToBeDisconnected.peripheral, deviceToBeDisconnected.timeIntervalBetweenLastConnectedAndLastAdvert > TimeInterval.minute else {
|
||||
return
|
||||
}
|
||||
logger.debug("taskIosMultiplex, multiplexing (toBeDisconnected=\(deviceToBeDisconnected.description))")
|
||||
disconnect("taskIosMultiplex", peripheralToBeDisconnected)
|
||||
pending.forEach() { device in
|
||||
guard let toBeConnected = device.peripheral else {
|
||||
return
|
||||
}
|
||||
connect("taskIosMultiplex|multiplex", toBeConnected);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Initiate next action on peripheral based on current state and information available
|
||||
private func taskInitiateNextAction(_ source: String, peripheral: CBPeripheral) {
|
||||
let targetIdentifier = TargetIdentifier(peripheral: peripheral)
|
||||
let device = database.device(peripheral, delegate: self)
|
||||
logger.debug("time since last payload=\(device.timeIntervalSinceLastPayloadDataUpdate)")
|
||||
if device.rssi == nil {
|
||||
// 1. RSSI
|
||||
logger.debug("taskInitiateNextAction (goal=rssi,peripheral=\(targetIdentifier))")
|
||||
readRSSI("taskInitiateNextAction|" + source, peripheral)
|
||||
} else if (device.signalCharacteristic == nil || device.payloadCharacteristic == nil) && device.legacyPayloadCharacteristic == nil {
|
||||
// 2. Characteristics
|
||||
logger.debug("taskInitiateNextAction (goal=characteristics,peripheral=\(targetIdentifier))")
|
||||
discoverServices("taskInitiateNextAction|" + source, peripheral)
|
||||
} else if device.payloadData == nil {
|
||||
// 3. Payload
|
||||
logger.debug("taskInitiateNextAction (goal=payload,peripheral=\(targetIdentifier))")
|
||||
readPayload("taskInitiateNextAction|" + source, device)
|
||||
} else if device.timeIntervalSinceLastPayloadDataUpdate > BluetraceConfig.PeripheralPayloadExpiry {
|
||||
// 4. Payload update
|
||||
logger.debug("taskInitiateNextAction (goal=payloadUpdate,peripheral=\(targetIdentifier),elapsed=\(device.timeIntervalSinceLastPayloadDataUpdate))")
|
||||
readPayload("taskInitiateNextAction|" + source, device)
|
||||
} else if device.operatingSystem != .ios {
|
||||
// 5. Disconnect Android
|
||||
logger.debug("taskInitiateNextAction (goal=disconnect|\(device.operatingSystem.rawValue),peripheral=\(targetIdentifier))")
|
||||
disconnect("taskInitiateNextAction|" + source, peripheral)
|
||||
} else {
|
||||
// 6. Scan
|
||||
logger.debug("taskInitiateNextAction (goal=scan,peripheral=\(targetIdentifier))")
|
||||
scheduleScan("taskInitiateNextAction|" + source)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Connect peripheral. Scanning is stopped temporarily, as recommended by Apple documentation, before initiating connect, otherwise
|
||||
pending scan operations tend to take priority and connect takes longer to start. Scanning is scheduled to resume later, to ensure scan
|
||||
resumes, even if connect fails.
|
||||
*/
|
||||
private func connect(_ source: String, _ peripheral: CBPeripheral) {
|
||||
let device = database.device(peripheral, delegate: self)
|
||||
logger.debug("connect (source=\(source),device=\(device))")
|
||||
guard central.state == .poweredOn else {
|
||||
logger.fault("connect denied, central not powered on (source=\(source),device=\(device))")
|
||||
return
|
||||
}
|
||||
queue.async {
|
||||
device.lastConnectRequestedAt = Date()
|
||||
self.central.retrievePeripherals(withIdentifiers: [peripheral.identifier]).forEach {
|
||||
if $0.state != .connected {
|
||||
// Check to see if Herald has initiated a connection attempt before
|
||||
if let lastAttempt = device.lastConnectionInitiationAttempt {
|
||||
// Has Herald already initiated a connect attempt?
|
||||
if (Date() > lastAttempt + BLESensorConfiguration.connectionAttemptTimeout) {
|
||||
// If timeout reached, force disconnect
|
||||
self.logger.fault("connect, timeout forcing disconnect (source=\(source),device=\(device),elapsed=\(-lastAttempt.timeIntervalSinceNow))")
|
||||
device.lastConnectionInitiationAttempt = nil
|
||||
self.queue.async { self.central.cancelPeripheralConnection(peripheral) }
|
||||
} else {
|
||||
// If not timed out yet, keep trying
|
||||
self.logger.debug("connect, retrying (source=\(source),device=\(device),elapsed=\(-lastAttempt.timeIntervalSinceNow))")
|
||||
self.central.connect($0)
|
||||
}
|
||||
} else {
|
||||
// If not, connect now
|
||||
self.logger.debug("connect, initiation (source=\(source),device=\(device))")
|
||||
device.lastConnectionInitiationAttempt = Date()
|
||||
self.central.connect($0)
|
||||
}
|
||||
} else {
|
||||
self.taskInitiateNextAction("connect|" + source, peripheral: $0)
|
||||
}
|
||||
}
|
||||
}
|
||||
scheduleScan("connect")
|
||||
}
|
||||
|
||||
/**
|
||||
Disconnect peripheral. On didDisconnect, a connect request will be made for iOS devices to maintain an open connection;
|
||||
there is no further action for Android. On didFailedToConnect, a connect request will be made for both iOS and Android
|
||||
devices as the error is likely to be transient (as described in Apple documentation), except if the error is "Device in invalid"
|
||||
then the peripheral is unregistered by removing it from the beacons table.
|
||||
*/
|
||||
private func disconnect(_ source: String, _ peripheral: CBPeripheral) {
|
||||
let targetIdentifier = TargetIdentifier(peripheral: peripheral)
|
||||
logger.debug("disconnect (source=\(source),peripheral=\(targetIdentifier))")
|
||||
guard peripheral.state == .connected || peripheral.state == .connecting else {
|
||||
logger.fault("disconnect denied, peripheral not connected or connecting (source=\(source),peripheral=\(targetIdentifier))")
|
||||
return
|
||||
}
|
||||
queue.async { self.central.cancelPeripheralConnection(peripheral) }
|
||||
}
|
||||
|
||||
/// Read RSSI
|
||||
private func readRSSI(_ source: String, _ peripheral: CBPeripheral) {
|
||||
let targetIdentifier = TargetIdentifier(peripheral: peripheral)
|
||||
logger.debug("readRSSI (source=\(source),peripheral=\(targetIdentifier))")
|
||||
guard peripheral.state == .connected else {
|
||||
logger.fault("readRSSI denied, peripheral not connected (source=\(source),peripheral=\(targetIdentifier))")
|
||||
scheduleScan("readRSSI")
|
||||
return
|
||||
}
|
||||
queue.async { peripheral.readRSSI() }
|
||||
}
|
||||
|
||||
/// Discover services
|
||||
private func discoverServices(_ source: String, _ peripheral: CBPeripheral) {
|
||||
let targetIdentifier = TargetIdentifier(peripheral: peripheral)
|
||||
logger.debug("discoverServices (source=\(source),peripheral=\(targetIdentifier))")
|
||||
guard peripheral.state == .connected else {
|
||||
logger.fault("discoverServices denied, peripheral not connected (source=\(source),peripheral=\(targetIdentifier))")
|
||||
scheduleScan("discoverServices")
|
||||
return
|
||||
}
|
||||
queue.async { peripheral.discoverServices([BLESensorConfiguration.serviceUUID]) }
|
||||
}
|
||||
|
||||
/// Read payload data from device
|
||||
private func readPayload(_ source: String, _ device: BLEDevice) {
|
||||
logger.debug("readPayload (source=\(source),peripheral=\(device.identifier))")
|
||||
guard let peripheral = device.peripheral, peripheral.state == .connected else {
|
||||
logger.fault("readPayload denied, peripheral not connected (source=\(source),peripheral=\(device.identifier))")
|
||||
return
|
||||
}
|
||||
guard let payloadCharacteristic = device.payloadCharacteristic != nil ? device.payloadCharacteristic : device.legacyPayloadCharacteristic else {
|
||||
logger.fault("readPayload denied, device missing payload characteristic (source=\(source),peripheral=\(device.identifier))")
|
||||
discoverServices("readPayload", peripheral)
|
||||
return
|
||||
}
|
||||
// De-duplicate read payload requests from multiple asynchronous calls
|
||||
let timeIntervalSinceLastReadPayloadRequestedAt = Date().timeIntervalSince(device.lastReadPayloadRequestedAt)
|
||||
guard timeIntervalSinceLastReadPayloadRequestedAt > 2 else {
|
||||
logger.fault("readPayload denied, duplicate request (source=\(source),peripheral=\(device.identifier),elapsed=\(timeIntervalSinceLastReadPayloadRequestedAt)")
|
||||
return
|
||||
}
|
||||
// Initiate read payload
|
||||
device.lastReadPayloadRequestedAt = Date()
|
||||
if device.operatingSystem == .android, let peripheral = device.peripheral {
|
||||
discoverServices("readPayload|android", peripheral)
|
||||
} else {
|
||||
queue.async { peripheral.readValue(for: payloadCharacteristic) }
|
||||
}
|
||||
}
|
||||
|
||||
/// legacy covidsafe device, existing covidsafe code will have the central \ receiver write to the peripheral after it has requested to read its payload
|
||||
|
||||
private func writeLegacyPayload(_ source: String, peripheral: CBPeripheral) {
|
||||
let device = database.device(peripheral, delegate: self)
|
||||
logger.debug("writeLegacyPayload (source=\(source),peripheral=\(device.identifier))")
|
||||
|
||||
guard device.rssi != nil else {
|
||||
logger.fault("writeLegacyPayload denied (source=\(source), rssi should be present in \(device.identifier) before write")
|
||||
return
|
||||
}
|
||||
guard let characteristic = device.legacyPayloadCharacteristic else {
|
||||
logger.fault("writeLegacyPayload denied (source=\(source),peripheral=\(device.identifier) legacyPayloadCharacteristic not present)")
|
||||
return
|
||||
}
|
||||
EncounterMessageManager.shared.getWritePayloadForCentral(device: device) { [weak self] (result) in
|
||||
self?.queue.async {
|
||||
guard let payloadToWrite = result else {
|
||||
self?.logger.fault("writeLegacyPayload denied (source=\(source),peripheral=\(device.identifier) failed to obtain tempId)")
|
||||
return
|
||||
}
|
||||
self?.logger.debug("writeLegacyPayload (source=\(source),peripheral=\(device.identifier) writing...)")
|
||||
peripheral.writeValue(payloadToWrite, for: characteristic, type: .withResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Wake transmitter by writing blank data to the beacon characteristic. This will trigger the transmitter to generate a data value update notification
|
||||
in 8 seconds, which in turn will trigger this receiver to receive a didUpdateValueFor call to keep both the transmitter and receiver awake, while
|
||||
maximising the time interval between bluetooth calls to minimise power usage.
|
||||
*/
|
||||
private func wakeTransmitter(_ source: String, _ device: BLEDevice) {
|
||||
guard device.operatingSystem == .ios, let peripheral = device.peripheral, let characteristic = device.signalCharacteristic else {
|
||||
return
|
||||
}
|
||||
logger.debug("wakeTransmitter (source=\(source),peripheral=\(device.identifier),write=\(characteristic.properties.contains(.write))")
|
||||
queue.async { peripheral.writeValue(self.emptyData, for: characteristic, type: .withResponse) }
|
||||
}
|
||||
|
||||
// MARK:- BLEDatabaseDelegate
|
||||
|
||||
func bleDatabase(didCreate device: BLEDevice) {
|
||||
// FEATURE : Symmetric connection on write
|
||||
// All CoreBluetooth delegate callbacks in BLETransmitter will register the central interacting with this peripheral
|
||||
// in the database and generate a didCreate callback here to trigger scan, which includes a task for resolving all
|
||||
// device identifiers to actual peripherals.
|
||||
scheduleScan("bleDatabase:didCreate (device=\(device.identifier))")
|
||||
}
|
||||
|
||||
// MARK:- CBCentralManagerDelegate
|
||||
|
||||
/// Reinstate devices following state restoration
|
||||
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
|
||||
// Restore -> Populate database
|
||||
logger.debug("willRestoreState")
|
||||
self.central = central
|
||||
central.delegate = self
|
||||
if let restoredPeripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] {
|
||||
for peripheral in restoredPeripherals {
|
||||
let targetIdentifier = TargetIdentifier(peripheral: peripheral)
|
||||
let device = database.device(peripheral, delegate: self)
|
||||
if device.operatingSystem == .unknown {
|
||||
device.operatingSystem = .restored
|
||||
}
|
||||
if peripheral.state == .connected {
|
||||
device.lastConnectedAt = Date()
|
||||
}
|
||||
logger.debug("willRestoreState (peripheral=\(targetIdentifier))")
|
||||
}
|
||||
}
|
||||
// Reconnection check performed in scan following centralManagerDidUpdateState:central.state == .powerOn
|
||||
}
|
||||
|
||||
/// Start scan when bluetooth is on.
|
||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
// Bluetooth on -> Scan
|
||||
if (central.state == .poweredOn) {
|
||||
logger.debug("Update state (state=poweredOn))")
|
||||
delegateQueue.async {
|
||||
self.delegates.forEach({ $0.sensor(.BLE, didUpdateState: .on) })
|
||||
self.connectionDelegate?.sensor(.BLE, didUpdateState: .on)
|
||||
}
|
||||
scan("updateState")
|
||||
} else {
|
||||
if #available(iOS 10.0, *) {
|
||||
logger.debug("Update state (state=\(central.state.description))")
|
||||
} else {
|
||||
// Required for compatibility with iOS 9.3
|
||||
switch central.state {
|
||||
case .poweredOff:
|
||||
logger.debug("Update state (state=poweredOff)")
|
||||
case .poweredOn:
|
||||
logger.debug("Update state (state=poweredOn)")
|
||||
case .resetting:
|
||||
logger.debug("Update state (state=resetting)")
|
||||
case .unauthorized:
|
||||
logger.debug("Update state (state=unauthorized)")
|
||||
case .unknown:
|
||||
logger.debug("Update state (state=unknown)")
|
||||
case .unsupported:
|
||||
logger.debug("Update state (state=unsupported)")
|
||||
default:
|
||||
logger.debug("Update state (state=undefined)")
|
||||
}
|
||||
}
|
||||
delegateQueue.async {
|
||||
self.delegates.forEach({ $0.sensor(.BLE, didUpdateState: .off) })
|
||||
self.connectionDelegate?.sensor(.BLE, didUpdateState: .off)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Share payload data across devices with the same pseudo device address
|
||||
private func shareDataAcrossDevices(_ pseudoDeviceAddress: BLEPseudoDeviceAddress) {
|
||||
// Get devices with the same pseudo address created recently
|
||||
let devicesWithSamePseudoAddress = database.devices().filter({ pseudoDeviceAddress.address == $0.pseudoDeviceAddress?.address && $0.timeIntervalSinceCreated <= BLESensorConfiguration.androidAdvertRefreshTimeInterval })
|
||||
// Get device with most recent version of payload amongst these devices
|
||||
guard let mostRecentDevice = devicesWithSamePseudoAddress.filter({ $0.payloadData != nil }).sorted(by: { $0.payloadDataLastUpdatedAt > $1.payloadDataLastUpdatedAt }).first, let payloadData = mostRecentDevice.payloadData else {
|
||||
return
|
||||
}
|
||||
// Copy data to all devices with the same pseudo address
|
||||
let payloadDataLastUpdatedAt = mostRecentDevice.payloadDataLastUpdatedAt
|
||||
let devicesToCopyPayload = devicesWithSamePseudoAddress.filter({ $0.payloadData == nil })
|
||||
devicesToCopyPayload.forEach({
|
||||
$0.signalCharacteristic = mostRecentDevice.signalCharacteristic
|
||||
$0.payloadCharacteristic = mostRecentDevice.payloadCharacteristic
|
||||
// Only Android devices have a pseudo address
|
||||
$0.operatingSystem = .android
|
||||
$0.payloadData = payloadData
|
||||
$0.payloadDataLastUpdatedAt = payloadDataLastUpdatedAt
|
||||
logger.debug("shareDataAcrossDevices, copied payload data (from=\(mostRecentDevice.description),to=\($0.description))")
|
||||
})
|
||||
// Get devices with the same payload
|
||||
let devicesWithSamePayload = database.devices().filter({ payloadData == $0.payloadData })
|
||||
// Copy pseudo address to all devices with the same payload
|
||||
let devicesToCopyAddress = devicesWithSamePayload.filter({ $0.pseudoDeviceAddress == nil })
|
||||
devicesToCopyAddress.forEach({
|
||||
$0.pseudoDeviceAddress = pseudoDeviceAddress
|
||||
logger.debug("shareDataAcrossDevices, copied pseudo address (payloadData=\(payloadData.shortName),to=\($0.description))")
|
||||
})
|
||||
}
|
||||
|
||||
/// Device discovery will trigger connection to resolve operating system and read payload for iOS and Android devices.
|
||||
/// Connection is kept active for iOS devices for on-going RSSI measurements, and closed for Android devices, as this
|
||||
/// iOS device can rely on this discovery callback (triggered by regular scan calls) for on-going RSSI and TX power
|
||||
/// updates, thus eliminating the need to keep connections open for Android devices that can cause stability issues for
|
||||
/// Android devices.
|
||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
||||
|
||||
// Populate device database
|
||||
let device = database.device(peripheral, delegate: self)
|
||||
device.lastDiscoveredAt = Date()
|
||||
device.rssi = BLE_RSSI(RSSI.intValue)
|
||||
|
||||
// We set android device to enable android discovery with legacy apps
|
||||
if let manuData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data, manuData.count > 2 {
|
||||
device.operatingSystem = .android
|
||||
}
|
||||
|
||||
if let pseudoDeviceAddress = BLEPseudoDeviceAddress(fromAdvertisementData: advertisementData) {
|
||||
device.pseudoDeviceAddress = pseudoDeviceAddress
|
||||
shareDataAcrossDevices(pseudoDeviceAddress)
|
||||
}
|
||||
if let txPower = (advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber)?.intValue {
|
||||
device.txPower = BLE_TxPower(txPower)
|
||||
}
|
||||
logger.debug("didDiscover (device=\(device),rssi=\((String(describing: device.rssi))),txPower=\((String(describing: device.txPower))))")
|
||||
if deviceHasPendingTask(device) {
|
||||
connect("didDiscover", peripheral);
|
||||
} else {
|
||||
scanResults.append(device)
|
||||
}
|
||||
// Schedule scan (actual connect is initiated from scan via prioritisation logic)
|
||||
scheduleScan("didDiscover")
|
||||
}
|
||||
|
||||
/// Successful connection to a device will initate the next pending action.
|
||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||
// connect -> readRSSI -> discoverServices
|
||||
let device = database.device(peripheral, delegate: self)
|
||||
device.lastConnectedAt = Date()
|
||||
logger.debug("didConnect (device=\(device))")
|
||||
taskInitiateNextAction("didConnect", peripheral: peripheral)
|
||||
}
|
||||
|
||||
/// Failure to connect to a device will result in de-registration for invalid devices or reconnection attempt otherwise.
|
||||
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
||||
// Connect fail -> Delete | Connect
|
||||
// Failure for peripherals advertising the beacon service should be transient, so try again.
|
||||
// This is also where iOS reports invalidated devices if connect is called after restore,
|
||||
// thus offers an opportunity for house keeping.
|
||||
let device = database.device(peripheral, delegate: self)
|
||||
logger.debug("didFailToConnect (device=\(device),error=\(String(describing: error)))")
|
||||
if String(describing: error).contains("Device is invalid") {
|
||||
logger.debug("Unregister invalid device (device=\(device))")
|
||||
database.delete(device.identifier)
|
||||
} else {
|
||||
connect("didFailToConnect", peripheral)
|
||||
}
|
||||
}
|
||||
|
||||
/// Graceful disconnection is usually caused by device going out of range or device changing identity, thus a reconnection call is initiated
|
||||
/// here for iOS devices to resume connection where possible. This is unnecessary for Android devices as they can be rediscovered by
|
||||
/// the regular scan calls. Please note, reconnection to iOS devices is likely to fail following prolonged period of being out of range as
|
||||
/// the target device is likely to have changed identity after about 20 minutes. This requires rediscovery which is impossible if the iOS device
|
||||
/// is in background state, hence the need for enabling location and screen on to trigger rediscovery (yes, its weird, but it works).
|
||||
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
||||
// Disconnected -> Connect if iOS
|
||||
// Keep connection only for iOS, not necessary for Android as they are always detectable
|
||||
let device = database.device(peripheral, delegate: self)
|
||||
device.lastDisconnectedAt = Date()
|
||||
logger.debug("didDisconnectPeripheral (device=\(device),error=\(String(describing: error)))")
|
||||
if device.operatingSystem == .ios {
|
||||
// Invalidate characteristics
|
||||
device.signalCharacteristic = nil
|
||||
device.payloadCharacteristic = nil
|
||||
device.legacyPayloadCharacteristic = nil
|
||||
// Reconnect
|
||||
connect("didDisconnectPeripheral", peripheral)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CBPeripheralDelegate
|
||||
|
||||
/// Read RSSI for proximity estimation.
|
||||
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
||||
// Read RSSI -> Read Code | Notify delegates -> Scan again
|
||||
// This is the primary loop for iOS after initial connection and subscription to
|
||||
// the notifying beacon characteristic. The loop is scan -> wakeTransmitter ->
|
||||
// didUpdateValueFor -> readRSSI -> notifyDelegates -> scheduleScan -> scan
|
||||
let device = database.device(peripheral, delegate: self)
|
||||
device.rssi = BLE_RSSI(RSSI.intValue)
|
||||
logger.debug("didReadRSSI (device=\(device),rssi=\(String(describing: device.rssi)),error=\(String(describing: error)))")
|
||||
taskInitiateNextAction("didReadRSSI", peripheral: peripheral)
|
||||
}
|
||||
|
||||
/// Service discovery triggers characteristic discovery.
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||
// Discover services -> Discover characteristics | Disconnect
|
||||
let device = database.device(peripheral, delegate: self)
|
||||
logger.debug("didDiscoverServices (device=\(device),error=\(String(describing: error)))")
|
||||
guard let services = peripheral.services else {
|
||||
disconnect("didDiscoverServices|serviceEmpty", peripheral)
|
||||
return
|
||||
}
|
||||
for service in services {
|
||||
if (service.uuid == BLESensorConfiguration.serviceUUID) {
|
||||
logger.debug("didDiscoverServices, found sensor service (device=\(device))")
|
||||
queue.async {
|
||||
peripheral.discoverCharacteristics([BLESensorConfiguration.legacyCovidsafePayloadCharacteristicUUID, BLESensorConfiguration.androidSignalCharacteristicUUID, BLESensorConfiguration.payloadCharacteristicUUID, BLESensorConfiguration.iosSignalCharacteristicUUID], for: service)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
disconnect("didDiscoverServices|serviceNotFound", peripheral)
|
||||
// The disconnect calls here shall be handled by didDisconnect which determines whether to retry for iOS or stop for Android
|
||||
}
|
||||
|
||||
/// Characteristic discovery provides definitive classification and confirmation of device operating system to inform next actions.
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||||
// Discover characteristics -> Notify delegates -> Disconnect | Wake transmitter -> Scan again
|
||||
let device = database.device(peripheral, delegate: self)
|
||||
logger.debug("didDiscoverCharacteristicsFor (device=\(device),error=\(String(describing: error)))")
|
||||
guard let characteristics = service.characteristics else {
|
||||
disconnect("didDiscoverCharacteristicsFor|characteristicEmpty", peripheral)
|
||||
return
|
||||
}
|
||||
for characteristic in characteristics {
|
||||
switch characteristic.uuid {
|
||||
case BLESensorConfiguration.androidSignalCharacteristicUUID:
|
||||
device.operatingSystem = .android
|
||||
device.signalCharacteristic = characteristic
|
||||
logger.debug("didDiscoverCharacteristicsFor, found android signal characteristic (device=\(device))")
|
||||
case BLESensorConfiguration.iosSignalCharacteristicUUID:
|
||||
// Maintain connection with iOS devices for keep awake
|
||||
let notify = characteristic.properties.contains(.notify)
|
||||
let write = characteristic.properties.contains(.write)
|
||||
device.operatingSystem = .ios
|
||||
device.signalCharacteristic = characteristic
|
||||
queue.async {
|
||||
peripheral.setNotifyValue(true, for: characteristic)
|
||||
}
|
||||
logger.debug("didDiscoverCharacteristicsFor, found ios signal characteristic (device=\(device),notify=\(notify),write=\(write))")
|
||||
case BLESensorConfiguration.payloadCharacteristicUUID:
|
||||
device.payloadCharacteristic = characteristic
|
||||
logger.debug("didDiscoverCharacteristicsFor, found payload characteristic (device=\(device))")
|
||||
case BLESensorConfiguration.legacyCovidsafePayloadCharacteristicUUID:
|
||||
// if we only have legacy characteristic, use it as will be a device with old version. Otherwise ignore and use new characteristics only.
|
||||
if characteristics.count == 1 {
|
||||
device.legacyPayloadCharacteristic = characteristic
|
||||
logger.debug("didDiscoverCharacteristicsFor, found covidsafe legacy payload characteristic (device=\(device))")
|
||||
} else {
|
||||
logger.debug("didDiscoverCharacteristicsFor, found covidsafe legacy payload characteristic but discarding as there are more characteristics, assuming new ble (device=\(device))")
|
||||
}
|
||||
default:
|
||||
logger.fault("didDiscoverCharacteristicsFor, found unknown characteristic (device=\(device),characteristic=\(characteristic.uuid))")
|
||||
}
|
||||
}
|
||||
// Android -> Read payload
|
||||
if device.operatingSystem == .android {
|
||||
var payloadCharacteristic = device.payloadCharacteristic
|
||||
if payloadCharacteristic == nil {
|
||||
payloadCharacteristic = device.legacyPayloadCharacteristic
|
||||
writeLegacyPayload("didDiscoverCharacteristicsFor|android", peripheral: peripheral)
|
||||
}
|
||||
if device.payloadData == nil || device.timeIntervalSinceLastPayloadDataUpdate > BluetraceConfig.PeripheralPayloadExpiry, let characteristicToRead = payloadCharacteristic {
|
||||
device.lastReadPayloadRequestedAt = Date()
|
||||
queue.async { peripheral.readValue(for: characteristicToRead) }
|
||||
} else {
|
||||
disconnect("didDiscoverCharacteristicsFor|android", peripheral)
|
||||
}
|
||||
}
|
||||
// Always -> Scan again
|
||||
// For initial connection, the scheduleScan call would have been made just before connect.
|
||||
// It is called again here to extend the time interval between scans.
|
||||
scheduleScan("didDiscoverCharacteristicsFor")
|
||||
}
|
||||
|
||||
/// This iOS device will write to connected iOS devices to keep them awake, and this call back provides a backup mechanism for keeping this
|
||||
/// device awake for longer in the event that other devices are no longer responding or in range.
|
||||
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
// Wrote characteristic -> Scan again
|
||||
let device = database.device(peripheral, delegate: self)
|
||||
logger.debug("didWriteValueFor (device=\(device),error=\(String(describing: error)))")
|
||||
// For all situations, scheduleScan would have been made earlier in the chain of async calls.
|
||||
// It is called again here to extend the time interval between scans, as this is usually the
|
||||
// last call made in all paths to wake the transmitter.
|
||||
scheduleScan("didWriteValueFor")
|
||||
}
|
||||
|
||||
/// Other iOS devices may refresh (stop/restart) their adverts at regular intervals, thus triggering this service modification callback
|
||||
/// to invalidate existing characteristics and reconnect to refresh the device data.
|
||||
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
|
||||
// iOS only
|
||||
// Modified service -> Invalidate beacon -> Scan
|
||||
let device = database.device(peripheral, delegate: self)
|
||||
let characteristics = invalidatedServices.map { $0.characteristics }.count
|
||||
logger.debug("didModifyServices (device=\(device),characteristics=\(characteristics))")
|
||||
guard characteristics == 0 else {
|
||||
return
|
||||
}
|
||||
device.signalCharacteristic = nil
|
||||
device.payloadCharacteristic = nil
|
||||
device.legacyPayloadCharacteristic = nil
|
||||
if peripheral.state == .connected {
|
||||
discoverServices("didModifyServices", peripheral)
|
||||
} else if peripheral.state != .connecting {
|
||||
connect("didModifyServices", peripheral)
|
||||
}
|
||||
}
|
||||
|
||||
/// All read characteristic requests will trigger this call back to handle the response.
|
||||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
// Updated value -> Read RSSI | Read Payload
|
||||
// Beacon characteristic is writable, primarily to enable non-transmitting Android devices to submit their
|
||||
// beacon code and RSSI as data to the transmitter via GATT write. The characteristic is also notifying on
|
||||
// iOS devices, to offer a mechanism for waking receivers. The process works as follows, (1) receiver writes
|
||||
// blank data to transmitter, (2) transmitter broadcasts value update notification after 8 seconds, (3)
|
||||
// receiver is woken up to handle didUpdateValueFor notification, (4) receiver calls readRSSI, (5) readRSSI
|
||||
// call completes and schedules scan after 8 seconds, (6) scan writes blank data to all iOS transmitters.
|
||||
// Process repeats to keep both iOS transmitters and receivers awake while maximising time interval between
|
||||
// bluetooth calls to minimise power usage.
|
||||
let device = database.device(peripheral, delegate: self)
|
||||
logger.debug("didUpdateValueFor (device=\(device),characteristic=\(characteristic.uuid),error=\(String(describing: error)))")
|
||||
switch characteristic.uuid {
|
||||
case BLESensorConfiguration.iosSignalCharacteristicUUID:
|
||||
// Wake up call from transmitter
|
||||
logger.debug("didUpdateValueFor (device=\(device),characteristic=iosSignalCharacteristic,error=\(String(describing: error)))")
|
||||
device.lastNotifiedAt = Date()
|
||||
readRSSI("didUpdateValueFor", peripheral)
|
||||
return
|
||||
case BLESensorConfiguration.androidSignalCharacteristicUUID:
|
||||
// Should not happen as Android signal is not notifying
|
||||
logger.fault("didUpdateValueFor (device=\(device),characteristic=androidSignalCharacteristic,error=\(String(describing: error)))")
|
||||
case BLESensorConfiguration.payloadCharacteristicUUID, BLESensorConfiguration.legacyCovidsafePayloadCharacteristicUUID:
|
||||
// Read payload data
|
||||
logger.debug("didUpdateValueFor (device=\(device),characteristic=payloadCharacteristic,error=\(String(describing: error)))")
|
||||
if let data = characteristic.value {
|
||||
device.payloadData = PayloadData(data)
|
||||
}
|
||||
if device.operatingSystem == .android {
|
||||
disconnect("didUpdateValueFor|payload|android", peripheral)
|
||||
}
|
||||
default:
|
||||
logger.fault("didUpdateValueFor, unknown characteristic (device=\(device),characteristic=\(characteristic.uuid),error=\(String(describing: error)))")
|
||||
}
|
||||
scheduleScan("didUpdateValueFor")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
214
CovidSafe/Herald/Sensor/BLE/BLESensor.swift
Normal file
214
CovidSafe/Herald/Sensor/BLE/BLESensor.swift
Normal file
|
@ -0,0 +1,214 @@
|
|||
//
|
||||
// BLESensor.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
protocol BLESensor : Sensor {
|
||||
}
|
||||
|
||||
/// Defines BLE sensor configuration data, e.g. service and characteristic UUIDs
|
||||
struct BLESensorConfiguration {
|
||||
#if DEBUG
|
||||
static let logLevel: SensorLoggerLevel = .debug;
|
||||
#else
|
||||
static let logLevel: SensorLoggerLevel = .fault;
|
||||
#endif
|
||||
/**
|
||||
Service UUID for beacon service. This is a fixed UUID to enable iOS devices to find each other even
|
||||
in background mode. Android devices will need to find Apple devices first using the manufacturer code
|
||||
then discover services to identify actual beacons.
|
||||
*/
|
||||
static let serviceUUID = BluetraceConfig.BluetoothServiceID
|
||||
///Signaling characteristic for controlling connection between peripheral and central, e.g. keep each other from suspend state
|
||||
///- Characteristic UUID is randomly generated V4 UUIDs that has been tested for uniqueness by conducting web searches to ensure it returns no results.
|
||||
public static var androidSignalCharacteristicUUID = CBUUID(string: "f617b813-092e-437a-8324-e09a80821a11")
|
||||
///Signaling characteristic for controlling connection between peripheral and central, e.g. keep each other from suspend state
|
||||
///- Characteristic UUID is randomly generated V4 UUIDs that has been tested for uniqueness by conducting web searches to ensure it returns no results.
|
||||
public static var iosSignalCharacteristicUUID = CBUUID(string: "0eb0d5f2-eae4-4a9a-8af3-a4adb02d4363")
|
||||
///Primary payload characteristic (read) for distributing payload data from peripheral to central, e.g. identity data
|
||||
///- Characteristic UUID is randomly generated V4 UUIDs that has been tested for uniqueness by conducting web searches to ensure it returns no results.
|
||||
public static var payloadCharacteristicUUID = CBUUID(string: "3e98c0f8-8f05-4829-a121-43e38f8933e7")
|
||||
static let legacyCovidsafePayloadCharacteristicUUID = BluetraceConfig.BluetoothServiceID
|
||||
/// Time delay between notifications for subscribers.
|
||||
static let notificationDelay = DispatchTimeInterval.seconds(8)
|
||||
/// Time delay between advert restart
|
||||
static let advertRestartTimeInterval = TimeInterval.hour
|
||||
/// Herald internal connection expiry timeout
|
||||
static let connectionAttemptTimeout = TimeInterval(12)
|
||||
/// Expiry time for shared payloads, to ensure only recently seen payloads are shared
|
||||
/// Must be > payloadSharingTimeInterval to share pending payloads
|
||||
static let payloadSharingExpiryTimeInterval = TimeInterval.minute * 5
|
||||
/// Maximum number of concurrent BLE connections
|
||||
static let concurrentConnectionQuota = 12
|
||||
/// Manufacturer data is being used on Android to store pseudo device address
|
||||
static let manufacturerIdForSensor = UInt16(65530);
|
||||
/// Advert refresh time interval on Android devices
|
||||
static let androidAdvertRefreshTimeInterval = TimeInterval.minute * 15;
|
||||
// Filter duplicate payload data and suppress sensor(didRead:fromTarget) delegate calls
|
||||
/// - Set to .never to disable this feature
|
||||
/// - Set time interval N to filter duplicate payload data seen in last N seconds
|
||||
/// - Example : 60 means filter duplicates in last minute
|
||||
/// - Filters all occurrences of payload data from all targets
|
||||
public static var filterDuplicatePayloadData = TimeInterval(30 * 60)
|
||||
|
||||
|
||||
/// Signal characteristic action code for write payload, expect 1 byte action code followed by 2 byte little-endian Int16 integer value for payload data length, then payload data
|
||||
static let signalCharacteristicActionWritePayload = UInt8(1)
|
||||
/// Signal characteristic action code for write RSSI, expect 1 byte action code followed by 4 byte little-endian Int32 integer value for RSSI value
|
||||
static let signalCharacteristicActionWriteRSSI = UInt8(2)
|
||||
/// Signal characteristic action code for write payload, expect 1 byte action code followed by 2 byte little-endian Int16 integer value for payload sharing data length, then payload sharing data
|
||||
static let signalCharacteristicActionWritePayloadSharing = UInt8(3)
|
||||
|
||||
/// Are Location Permissions enabled in the app, and thus awake on screen on enabled
|
||||
public static var awakeOnLocationEnabled: Bool = true
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
BLE sensor based on CoreBluetooth
|
||||
Requires : Signing & Capabilities : BackgroundModes : Uses Bluetooth LE accessories = YES
|
||||
Requires : Signing & Capabilities : BackgroundModes : Acts as a Bluetooth LE accessory = YES
|
||||
Requires : Info.plist : Privacy - Bluetooth Always Usage Description
|
||||
Requires : Info.plist : Privacy - Bluetooth Peripheral Usage Description
|
||||
*/
|
||||
class ConcreteBLESensor : NSObject, BLESensor, BLEDatabaseDelegate {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "BLE.ConcreteBLESensor")
|
||||
private let sensorQueue = DispatchQueue(label: "Sensor.BLE.ConcreteBLESensor.SensorQueue")
|
||||
private let delegateQueue = DispatchQueue(label: "Sensor.BLE.ConcreteBLESensor.DelegateQueue")
|
||||
private var delegates: [SensorDelegate] = []
|
||||
private let database: BLEDatabase
|
||||
private let transmitter: BLETransmitter
|
||||
private let receiver: BLEReceiver
|
||||
// Record payload data to enable de-duplication
|
||||
private var didReadPayloadData: [PayloadData:Date] = [:]
|
||||
|
||||
init(_ payloadDataSupplier: PayloadDataSupplier) {
|
||||
database = ConcreteBLEDatabase()
|
||||
transmitter = ConcreteBLETransmitter(queue: sensorQueue, delegateQueue: delegateQueue, database: database, payloadDataSupplier: payloadDataSupplier)
|
||||
receiver = ConcreteBLEReceiver(queue: sensorQueue,delegateQueue: delegateQueue, database: database, payloadDataSupplier: payloadDataSupplier)
|
||||
super.init()
|
||||
database.add(delegate: self)
|
||||
}
|
||||
|
||||
func start() {
|
||||
logger.debug("start")
|
||||
|
||||
var permissionRequested = false
|
||||
if #available(iOS 13.1, *) {
|
||||
permissionRequested = (CBManager.authorization != .notDetermined)
|
||||
} else {
|
||||
// todo: consider iOS 13.0, which has different behavior from 13.0
|
||||
permissionRequested = CBPeripheralManager.authorizationStatus() != .notDetermined
|
||||
}
|
||||
|
||||
if let receiver = receiver as? ConcreteBLEReceiver, !permissionRequested {
|
||||
// BLE receivers start on powerOn event, on status change the transmitter will be started.
|
||||
// This is to request permissions and turn on dialogs sequentially when registering
|
||||
receiver.addConnectionDelegate(delegate: self)
|
||||
}
|
||||
receiver.start()
|
||||
|
||||
// if permissions have been requested start transmitter immediately
|
||||
if permissionRequested {
|
||||
transmitter.start()
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
logger.debug("stop")
|
||||
transmitter.stop()
|
||||
receiver.stop()
|
||||
// BLE transmitter and receivers stops on powerOff event
|
||||
}
|
||||
|
||||
func add(delegate: SensorDelegate) {
|
||||
delegates.append(delegate)
|
||||
transmitter.add(delegate: delegate)
|
||||
receiver.add(delegate: delegate)
|
||||
}
|
||||
|
||||
// MARK:- BLEDatabaseDelegate
|
||||
|
||||
func bleDatabase(didCreate device: BLEDevice) {
|
||||
logger.debug("didDetect (device=\(device.identifier),payloadData=\(device.payloadData?.shortName ?? "nil"))")
|
||||
delegateQueue.async {
|
||||
self.delegates.forEach { $0.sensor(.BLE, didDetect: device.identifier) }
|
||||
}
|
||||
}
|
||||
|
||||
func bleDatabase(didUpdate device: BLEDevice, attribute: BLEDeviceAttribute) {
|
||||
switch attribute {
|
||||
case .rssi:
|
||||
guard let rssi = device.rssi else {
|
||||
return
|
||||
}
|
||||
let proximity = Proximity(unit: .RSSI, value: Double(rssi))
|
||||
logger.debug("didMeasure (device=\(device.identifier),payloadData=\(device.payloadData?.shortName ?? "nil"),proximity=\(proximity.description))")
|
||||
delegateQueue.async {
|
||||
self.delegates.forEach { $0.sensor(.BLE, didMeasure: proximity, fromTarget: device.identifier) }
|
||||
}
|
||||
guard let payloadData = device.payloadData else {
|
||||
return
|
||||
}
|
||||
delegateQueue.async {
|
||||
self.delegates.forEach { $0.sensor(.BLE, didMeasure: proximity, fromTarget: device.identifier, withPayload: payloadData, forDevice: device) }
|
||||
}
|
||||
case .payloadData:
|
||||
guard let payloadData = device.payloadData else {
|
||||
return
|
||||
}
|
||||
guard device.lastReadPayloadRequestedAt != Date.distantPast else {
|
||||
logger.debug("didRead payload. lastReadPayloadRequestedAt is not set and payload has been updated. This is an android data share/copy and is ignored.")
|
||||
return
|
||||
}
|
||||
logger.debug("didRead (device=\(device.identifier),payloadData=\(payloadData.shortName))")
|
||||
guard let rssi = device.rssi else {
|
||||
logger.debug("didRead rssi is nil, not proceeding")
|
||||
return
|
||||
}
|
||||
// De-duplicate payload in recent time
|
||||
if BLESensorConfiguration.filterDuplicatePayloadData != .never {
|
||||
let removePayloadDataBefore = Date() - BLESensorConfiguration.filterDuplicatePayloadData
|
||||
let recentDidReadPayloadData = didReadPayloadData.filter({ $0.value >= removePayloadDataBefore })
|
||||
didReadPayloadData = recentDidReadPayloadData
|
||||
if let lastReportedAt = didReadPayloadData[payloadData] {
|
||||
logger.debug("didRead, filtered duplicate (device=\(device.identifier),payloadData=\(payloadData.shortName),lastReportedAt=\(lastReportedAt.description))")
|
||||
return
|
||||
}
|
||||
didReadPayloadData[payloadData] = Date()
|
||||
}
|
||||
|
||||
let proximity = Proximity(unit: .RSSI, value: Double(rssi))
|
||||
delegateQueue.async {
|
||||
self.delegates.forEach { $0.sensor(.BLE, didRead: payloadData, fromTarget: device.identifier, atProximity: proximity, withTxPower: device.txPower) }
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ConcreteBLESensor: SensorDelegate {
|
||||
func sensor(_ sensor: SensorType, didUpdateState: SensorState) {
|
||||
guard let receiver = receiver as? ConcreteBLEReceiver else {
|
||||
return
|
||||
}
|
||||
receiver.removeConnectionDelegate()
|
||||
transmitter.start()
|
||||
}
|
||||
}
|
||||
|
||||
extension TargetIdentifier {
|
||||
init(peripheral: CBPeripheral) {
|
||||
self.init(peripheral.identifier.uuidString)
|
||||
}
|
||||
init(central: CBCentral) {
|
||||
self.init(central.identifier.uuidString)
|
||||
}
|
||||
}
|
553
CovidSafe/Herald/Sensor/BLE/BLETransmitter.swift
Normal file
553
CovidSafe/Herald/Sensor/BLE/BLETransmitter.swift
Normal file
|
@ -0,0 +1,553 @@
|
|||
//
|
||||
// BLETransmitter.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
/**
|
||||
Beacon transmitter broadcasts a fixed service UUID to enable background scan by iOS. When iOS
|
||||
enters background mode, the UUID will disappear from the broadcast, so Android devices need to
|
||||
search for Apple devices and then connect and discover services to read the UUID.
|
||||
*/
|
||||
protocol BLETransmitter : Sensor {
|
||||
}
|
||||
|
||||
/**
|
||||
Transmitter offers two services:
|
||||
1. Signal characteristic for maintaining connection between iOS devices and also enable non-transmitting Android devices (receive only,
|
||||
like the Samsung J6) to make their presence known by writing their beacon code and RSSI as data to this characteristic.
|
||||
2. Payload characteristic for publishing beacon identity data.
|
||||
|
||||
Keeping the transmitter and receiver working in iOS background mode is a major challenge, in particular when both
|
||||
iOS devices are in background mode. The transmitter on iOS offers a notifying beacon characteristic that is triggered
|
||||
by writing anything to the characteristic. On characteristic write, the transmitter will call updateValue after 8 seconds
|
||||
to notify the receivers, to wake up the receivers with a didUpdateValueFor call. The process can repeat as a loop
|
||||
between the transmitter and receiver to keep both devices awake. This is unnecessary for Android-Android and also
|
||||
Android-iOS and iOS-Android detection, which can rely solely on scanForPeripherals for detection.
|
||||
|
||||
The notification based wake up method relies on an open connection which seems to be fine for iOS but may cause
|
||||
problems for Android. Experiments have found that Android devices cannot accept new connections (without explicit
|
||||
disconnect) indefinitely and the bluetooth stack ceases to function after around 500 open connections. The device
|
||||
will need to be rebooted to recover. However, if each connection is disconnected, the bluetooth stack can work
|
||||
indefinitely, but frequent connect and disconnect can still cause the same problem. The recommendation is to
|
||||
(1) always disconnect from Android as soon as the work is complete, (2) minimise the number of connections to
|
||||
an Android device, and (3) maximise time interval between connections. With all these in mind, the transmitter
|
||||
on Android does not support notify and also a connect is only performed on first contact to get the bacon code.
|
||||
*/
|
||||
class ConcreteBLETransmitter : NSObject, BLETransmitter, CBPeripheralManagerDelegate {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "BLE.ConcreteBLETransmitter")
|
||||
private var delegates: [SensorDelegate] = []
|
||||
/// Dedicated sequential queue for all beacon transmitter and receiver tasks.
|
||||
private let queue: DispatchQueue
|
||||
private let delegateQueue: DispatchQueue
|
||||
private let database: BLEDatabase
|
||||
/// Beacon code generator for creating cryptographically secure public codes that can be later used for on-device matching.
|
||||
private let payloadDataSupplier: PayloadDataSupplier
|
||||
/// Peripheral manager for managing all connections, using a single manager for simplicity.
|
||||
private var peripheral: CBPeripheralManager!
|
||||
/// Beacon service and characteristics being broadcasted by the transmitter.
|
||||
private var signalCharacteristic: CBMutableCharacteristic?
|
||||
private var payloadCharacteristic: CBMutableCharacteristic?
|
||||
private var legacyCovidPayloadCharacteristic: CBMutableCharacteristic?
|
||||
private var advertisingStartedAt: Date = Date.distantPast
|
||||
/// Dummy data for writing to the receivers to trigger state restoration or resume from suspend state to background state.
|
||||
private let emptyData = Data(repeating: 0, count: 0)
|
||||
/**
|
||||
Shifting timer for triggering notify for subscribers several seconds after resume from suspend state to background state,
|
||||
but before re-entering suspend state. The time limit is under 10 seconds as desribed in Apple documentation.
|
||||
*/
|
||||
private var notifyTimer: DispatchSourceTimer?
|
||||
/// Dedicated sequential queue for the shifting timer.
|
||||
private let notifyTimerQueue = DispatchQueue(label: "Sensor.BLE.ConcreteBLETransmitter.Timer")
|
||||
|
||||
/**
|
||||
Create a transmitter that uses the same sequential dispatch queue as the receiver.
|
||||
Transmitter starts automatically when Bluetooth is enabled.
|
||||
*/
|
||||
init(queue: DispatchQueue, delegateQueue: DispatchQueue, database: BLEDatabase, payloadDataSupplier: PayloadDataSupplier) {
|
||||
self.queue = queue
|
||||
self.delegateQueue = delegateQueue
|
||||
self.database = database
|
||||
self.payloadDataSupplier = payloadDataSupplier
|
||||
super.init()
|
||||
|
||||
}
|
||||
|
||||
func add(delegate: SensorDelegate) {
|
||||
delegates.append(delegate)
|
||||
}
|
||||
|
||||
func start() {
|
||||
logger.debug("start")
|
||||
|
||||
// Create a peripheral that supports state restoration
|
||||
if peripheral == nil {
|
||||
self.peripheral = CBPeripheralManager(delegate: self, queue: queue, options: [
|
||||
CBPeripheralManagerOptionRestoreIdentifierKey : "Sensor.BLE.ConcreteBLETransmitter",
|
||||
CBPeripheralManagerOptionShowPowerAlertKey : true
|
||||
])
|
||||
}
|
||||
|
||||
guard peripheral.state == .poweredOn else {
|
||||
logger.fault("start denied, not powered on")
|
||||
return
|
||||
}
|
||||
if signalCharacteristic != nil, payloadCharacteristic != nil, legacyCovidPayloadCharacteristic != nil {
|
||||
logger.debug("starting advert with existing characteristics")
|
||||
if !peripheral.isAdvertising {
|
||||
startAdvertising(withNewCharacteristics: false)
|
||||
} else {
|
||||
queue.async {
|
||||
self.peripheral.stopAdvertising()
|
||||
self.peripheral.startAdvertising([CBAdvertisementDataServiceUUIDsKey : [BLESensorConfiguration.serviceUUID]])
|
||||
}
|
||||
}
|
||||
logger.debug("start successful, for existing characteristics")
|
||||
} else {
|
||||
startAdvertising(withNewCharacteristics: true)
|
||||
logger.debug("start successful, for new characteristics")
|
||||
}
|
||||
signalCharacteristic?.subscribedCentrals?.forEach() { central in
|
||||
// FEATURE : Symmetric connection on subscribe
|
||||
_ = database.device(central.identifier.uuidString)
|
||||
}
|
||||
notifySubscribers("start")
|
||||
}
|
||||
|
||||
func stop() {
|
||||
logger.debug("stop")
|
||||
guard peripheral != nil else {
|
||||
return
|
||||
}
|
||||
guard peripheral.isAdvertising else {
|
||||
logger.fault("stop denied, already stopped (source=%s)")
|
||||
self.peripheral = nil
|
||||
return
|
||||
}
|
||||
stopAdvertising()
|
||||
}
|
||||
|
||||
private func startAdvertising(withNewCharacteristics: Bool) {
|
||||
logger.debug("startAdvertising (withNewCharacteristics=\(withNewCharacteristics))")
|
||||
if withNewCharacteristics || signalCharacteristic == nil || payloadCharacteristic == nil || legacyCovidPayloadCharacteristic == nil {
|
||||
signalCharacteristic = CBMutableCharacteristic(type: BLESensorConfiguration.iosSignalCharacteristicUUID, properties: [.write, .notify], value: nil, permissions: [.writeable])
|
||||
payloadCharacteristic = CBMutableCharacteristic(type: BLESensorConfiguration.payloadCharacteristicUUID, properties: [.read], value: nil, permissions: [.readable])
|
||||
legacyCovidPayloadCharacteristic = CBMutableCharacteristic(type: BluetraceConfig.BluetoothServiceID, properties: [.read, .write, .writeWithoutResponse], value: nil, permissions: [.readable, .writeable])
|
||||
}
|
||||
let service = CBMutableService(type: BLESensorConfiguration.serviceUUID, primary: true)
|
||||
signalCharacteristic?.value = nil
|
||||
payloadCharacteristic?.value = nil
|
||||
legacyCovidPayloadCharacteristic?.value = nil
|
||||
service.characteristics = [signalCharacteristic!, payloadCharacteristic!, legacyCovidPayloadCharacteristic!]
|
||||
queue.async {
|
||||
self.peripheral.stopAdvertising()
|
||||
self.peripheral.removeAllServices()
|
||||
self.peripheral.add(service)
|
||||
self.peripheral.startAdvertising([CBAdvertisementDataServiceUUIDsKey : [BLESensorConfiguration.serviceUUID]])
|
||||
}
|
||||
}
|
||||
|
||||
private func stopAdvertising() {
|
||||
logger.debug("stopAdvertising()")
|
||||
queue.async {
|
||||
self.peripheral.stopAdvertising()
|
||||
self.peripheral = nil
|
||||
}
|
||||
notifyTimer?.cancel()
|
||||
notifyTimer = nil
|
||||
}
|
||||
|
||||
/// All work starts from notify subscribers loop.
|
||||
/// Generate updateValue notification after 8 seconds to notify all subscribers and keep the iOS receivers awake.
|
||||
private func notifySubscribers(_ source: String) {
|
||||
notifyTimer?.cancel()
|
||||
notifyTimer = DispatchSource.makeTimerSource(queue: notifyTimerQueue)
|
||||
notifyTimer?.schedule(deadline: DispatchTime.now() + BLESensorConfiguration.notificationDelay)
|
||||
notifyTimer?.setEventHandler { [weak self] in
|
||||
guard let s = self, let logger = self?.logger, let signalCharacteristic = self?.signalCharacteristic else {
|
||||
return
|
||||
}
|
||||
// Notify subscribers to keep them awake
|
||||
s.queue.async {
|
||||
logger.debug("notifySubscribers (source=\(source))")
|
||||
s.peripheral.updateValue(s.emptyData, for: signalCharacteristic, onSubscribedCentrals: nil)
|
||||
}
|
||||
// Restart advert if required
|
||||
let advertUpTime = Date().timeIntervalSince(s.advertisingStartedAt)
|
||||
if s.peripheral.isAdvertising, advertUpTime > BLESensorConfiguration.advertRestartTimeInterval {
|
||||
logger.debug("advertRestart (upTime=\(advertUpTime))")
|
||||
s.startAdvertising(withNewCharacteristics: true)
|
||||
}
|
||||
}
|
||||
notifyTimer?.resume()
|
||||
}
|
||||
|
||||
// MARK:- CBPeripheralManagerDelegate
|
||||
|
||||
/// Restore advert and reinstate advertised characteristics.
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, willRestoreState dict: [String : Any]) {
|
||||
logger.debug("willRestoreState")
|
||||
self.peripheral = peripheral
|
||||
peripheral.delegate = self
|
||||
if let services = dict[CBPeripheralManagerRestoredStateServicesKey] as? [CBMutableService] {
|
||||
for service in services {
|
||||
logger.debug("willRestoreState (service=\(service.uuid.uuidString))")
|
||||
if let characteristics = service.characteristics {
|
||||
for characteristic in characteristics {
|
||||
logger.debug("willRestoreState (characteristic=\(characteristic.uuid.uuidString))")
|
||||
switch characteristic.uuid {
|
||||
case BLESensorConfiguration.androidSignalCharacteristicUUID:
|
||||
if let mutableCharacteristic = characteristic as? CBMutableCharacteristic {
|
||||
signalCharacteristic = mutableCharacteristic
|
||||
logger.debug("willRestoreState (androidSignalCharacteristic=\(characteristic.uuid.uuidString))")
|
||||
} else {
|
||||
logger.fault("willRestoreState characteristic not mutable (androidSignalCharacteristic=\(characteristic.uuid.uuidString))")
|
||||
}
|
||||
case BLESensorConfiguration.iosSignalCharacteristicUUID:
|
||||
if let mutableCharacteristic = characteristic as? CBMutableCharacteristic {
|
||||
signalCharacteristic = mutableCharacteristic
|
||||
logger.debug("willRestoreState (iosSignalCharacteristic=\(characteristic.uuid.uuidString))")
|
||||
} else {
|
||||
logger.fault("willRestoreState characteristic not mutable (iosSignalCharacteristic=\(characteristic.uuid.uuidString))")
|
||||
}
|
||||
case BLESensorConfiguration.payloadCharacteristicUUID:
|
||||
if let mutableCharacteristic = characteristic as? CBMutableCharacteristic {
|
||||
payloadCharacteristic = mutableCharacteristic
|
||||
logger.debug("willRestoreState (payloadCharacteristic=\(characteristic.uuid.uuidString))")
|
||||
} else {
|
||||
logger.fault("willRestoreState characteristic not mutable (payloadCharacteristic=\(characteristic.uuid.uuidString))")
|
||||
}
|
||||
default:
|
||||
logger.debug("willRestoreState (unknownCharacteristic=\(characteristic.uuid.uuidString))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start advertising on bluetooth power on.
|
||||
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
|
||||
// Bluetooth on -> Advertise
|
||||
if (peripheral.state == .poweredOn) {
|
||||
logger.debug("Update state (state=poweredOn)")
|
||||
start()
|
||||
} else {
|
||||
if #available(iOS 10.0, *) {
|
||||
logger.debug("Update state (state=\(peripheral.state.description))")
|
||||
} else {
|
||||
// Required to support iOS 9.3
|
||||
switch peripheral.state {
|
||||
case .poweredOff:
|
||||
logger.debug("Update state (state=poweredOff)")
|
||||
case .poweredOn:
|
||||
logger.debug("Update state (state=poweredOn)")
|
||||
case .resetting:
|
||||
logger.debug("Update state (state=resetting)")
|
||||
case .unauthorized:
|
||||
logger.debug("Update state (state=unauthorized)")
|
||||
case .unknown:
|
||||
logger.debug("Update state (state=unknown)")
|
||||
case .unsupported:
|
||||
logger.debug("Update state (state=unsupported)")
|
||||
default:
|
||||
logger.debug("Update state (state=undefined)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
|
||||
logger.debug("peripheralManagerDidStartAdvertising (error=\(String(describing: error)))")
|
||||
if error == nil {
|
||||
advertisingStartedAt = Date()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Write request offers a mechanism for non-transmitting BLE devices (e.g. Samsung J6 can only receive) to make
|
||||
its presence known by submitting its beacon code and RSSI as data. This also offers a mechanism for iOS to
|
||||
write blank data to transmitter to keep bringing it back from suspended state to background state which increases
|
||||
its chance of background scanning over a long period without being killed off. Payload sharing is also based on
|
||||
write characteristic to enable Android peers to act as a bridge for sharing iOS device payloads, thus enabling
|
||||
iOS - iOS background detection without location permission or screen on, as background detection and tracking method.
|
||||
*/
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
|
||||
// Write -> Notify delegates -> Write response -> Notify subscribers
|
||||
for request in requests {
|
||||
let targetIdentifier = TargetIdentifier(central: request.central)
|
||||
// FEATURE : Symmetric connection on write
|
||||
let targetDevice = database.device(targetIdentifier)
|
||||
logger.debug("didReceiveWrite (central=\(targetIdentifier))")
|
||||
if let data = request.value {
|
||||
guard request.characteristic.uuid != legacyCovidPayloadCharacteristic?.uuid else {
|
||||
logger.debug("didReceiveWrite (central=\(targetIdentifier),action=writeLegacyCovidPayload)")
|
||||
|
||||
// we don't do anything with the payload.
|
||||
// Herald relies only on reads. Therefore when legacy writes we ignore.
|
||||
// However, to maintain legacy data as expected, payload is still written after read.
|
||||
// See BLEReceiver writeLegacyPayload
|
||||
queue.async { peripheral.respond(to: request, withResult: .success) }
|
||||
continue
|
||||
}
|
||||
if data.count == 0 {
|
||||
// Receiver writes blank data on detection of transmitter to bring iOS transmitter back from suspended state
|
||||
logger.debug("didReceiveWrite (central=\(targetIdentifier),action=wakeTransmitter)")
|
||||
queue.async { peripheral.respond(to: request, withResult: .success) }
|
||||
} else if let actionCode = data.uint8(0) {
|
||||
switch actionCode {
|
||||
case BLESensorConfiguration.signalCharacteristicActionWritePayload:
|
||||
// Receive-only Android device writing its payload to make its presence known
|
||||
logger.debug("didReceiveWrite (central=\(targetIdentifier),action=writePayload)")
|
||||
// writePayload data format
|
||||
// 0-0 : actionCode
|
||||
// 1-2 : payload data count in bytes (Int16)
|
||||
// 3.. : payload data
|
||||
if let payloadDataCount = data.int16(1) {
|
||||
logger.debug("didReceiveWrite -> didDetect=\(targetIdentifier)")
|
||||
delegateQueue.async {
|
||||
self.delegates.forEach { $0.sensor(.BLE, didDetect: targetIdentifier) }
|
||||
}
|
||||
if data.count == (3 + payloadDataCount) {
|
||||
let payloadData = PayloadData(data.subdata(in: 3..<data.count))
|
||||
logger.debug("didReceiveWrite -> didRead=\(payloadData.shortName),fromTarget=\(targetIdentifier)")
|
||||
targetDevice.operatingSystem = .android
|
||||
targetDevice.receiveOnly = true
|
||||
targetDevice.payloadData = payloadData
|
||||
queue.async { peripheral.respond(to: request, withResult: .success) }
|
||||
} else {
|
||||
logger.fault("didReceiveWrite, invalid payload (central=\(targetIdentifier),action=writePayload)")
|
||||
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
|
||||
}
|
||||
} else {
|
||||
logger.fault("didReceiveWrite, invalid request (central=\(targetIdentifier),action=writePayload)")
|
||||
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
|
||||
|
||||
}
|
||||
case BLESensorConfiguration.signalCharacteristicActionWriteRSSI:
|
||||
// Receive-only Android device writing its RSSI to make its proximity known
|
||||
logger.debug("didReceiveWrite (central=\(targetIdentifier),action=writeRSSI)")
|
||||
// writeRSSI data format
|
||||
// 0-0 : actionCode
|
||||
// 1-2 : rssi value (Int16)
|
||||
if let rssi = data.int16(1) {
|
||||
let proximity = Proximity(unit: .RSSI, value: Double(rssi))
|
||||
logger.debug("didReceiveWrite -> didMeasure=\(proximity.description),fromTarget=\(targetIdentifier)")
|
||||
targetDevice.operatingSystem = .android
|
||||
targetDevice.receiveOnly = true
|
||||
targetDevice.rssi = BLE_RSSI(rssi)
|
||||
queue.async { peripheral.respond(to: request, withResult: .success) }
|
||||
} else {
|
||||
logger.fault("didReceiveWrite, invalid request (central=\(targetIdentifier),action=writeRSSI)")
|
||||
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
|
||||
}
|
||||
case BLESensorConfiguration.signalCharacteristicActionWritePayloadSharing:
|
||||
// Android device sharing detected iOS devices with this iOS device to enable background detection
|
||||
logger.debug("didReceiveWrite (central=\(targetIdentifier),action=writePayloadSharing)")
|
||||
// writePayloadSharing data format
|
||||
// 0-0 : actionCode
|
||||
// 1-2 : rssi value (Int16)
|
||||
// 3-4 : payload sharing data count in bytes (Int16)
|
||||
// 5.. : payload sharing data (to be parsed by payload data supplier)
|
||||
if let rssi = data.int16(1), let payloadDataCount = data.int16(3) {
|
||||
// skip if a payload with length 0 is sent
|
||||
if data.count == (5 + payloadDataCount) && payloadDataCount > 0 {
|
||||
let payloadSharingData = payloadDataSupplier.payload(data.subdata(in: 5..<data.count))
|
||||
logger.debug("didReceiveWrite -> didShare=\(payloadSharingData.description),fromTarget=\(targetIdentifier)")
|
||||
let proximity = Proximity(unit: .RSSI, value: Double(rssi))
|
||||
|
||||
var filteredPayloadSharingData: [PayloadData]
|
||||
if let cachedPayload = EncounterMessageManager.shared.getLastKnownAdvertisementPayload(identifier: request.central.identifier) {
|
||||
// check that the shared data is not the data sent to devices we are receiving from and it does not exist already
|
||||
filteredPayloadSharingData = payloadSharingData.filter({ (dataToCheck) -> Bool in
|
||||
return dataToCheck != cachedPayload && !self.database.hasDevice(dataToCheck)
|
||||
})
|
||||
} else {
|
||||
// check that it does not exist already
|
||||
filteredPayloadSharingData = payloadSharingData.filter({ (dataToCheck) -> Bool in
|
||||
return !self.database.hasDevice(dataToCheck)
|
||||
})
|
||||
}
|
||||
|
||||
self.logger.debug("didReceiveWrite -> filtered didShare=\(filteredPayloadSharingData.description),fromTarget=\(targetIdentifier)")
|
||||
|
||||
queue.async { peripheral.respond(to: request, withResult: .success) }
|
||||
|
||||
|
||||
targetDevice.operatingSystem = .android
|
||||
targetDevice.rssi = BLE_RSSI(rssi)
|
||||
filteredPayloadSharingData.forEach() { payloadData in
|
||||
logger.debug("didReceiveWrite, storing device with shared payload=\(payloadData.shortName)")
|
||||
let sharedDevice = self.database.device(payloadData)
|
||||
if sharedDevice.operatingSystem == .unknown {
|
||||
sharedDevice.operatingSystem = .shared
|
||||
}
|
||||
sharedDevice.rssi = BLE_RSSI(rssi)
|
||||
|
||||
self.delegateQueue.async {
|
||||
self.delegates.forEach {
|
||||
$0.sensor(.BLE, didShare: filteredPayloadSharingData, fromTarget: sharedDevice.identifier, atProximity: proximity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.fault("didReceiveWrite, invalid payload (central=\(targetIdentifier),action=writePayloadSharing)")
|
||||
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
|
||||
}
|
||||
} else {
|
||||
logger.fault("didReceiveWrite, invalid request (central=\(targetIdentifier),action=writePayloadSharing)")
|
||||
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
|
||||
}
|
||||
default:
|
||||
logger.fault("didReceiveWrite (central=\(targetIdentifier),action=unknown,actionCode=\(actionCode))")
|
||||
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
|
||||
}
|
||||
}
|
||||
notifySubscribers("didReceiveWrite")
|
||||
}
|
||||
|
||||
/// Read request from central for obtaining payload data from this peripheral.
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
|
||||
// Read -> Notify subscribers
|
||||
let central = database.device(TargetIdentifier(request.central.identifier.uuidString))
|
||||
switch request.characteristic.uuid {
|
||||
case BLESensorConfiguration.payloadCharacteristicUUID, BLESensorConfiguration.legacyCovidsafePayloadCharacteristicUUID:
|
||||
logger.debug("Read received (central=\(central.description),characteristic=payload,offset=\(request.offset))")
|
||||
payloadDataSupplier.payload(request.central.identifier,
|
||||
offset: request.offset) { [self] (payloadData) in
|
||||
queue.async {
|
||||
guard let data = payloadData else {
|
||||
peripheral.respond(to: request, withResult: .unlikelyError)
|
||||
return
|
||||
}
|
||||
logger.debug("Read received (central=\(central.description),characteristic=\(request.characteristic.uuid),payload=\(data.shortName))")
|
||||
guard request.offset < data.count else {
|
||||
logger.fault("Read, invalid offset (central=\(central.description),characteristic=payload,offset=\(request.offset),data=\(data.count))")
|
||||
peripheral.respond(to: request, withResult: .invalidOffset)
|
||||
return
|
||||
}
|
||||
guard request.offset != data.count else {
|
||||
// the receiver already read all the data in its last read request
|
||||
peripheral.respond(to: request, withResult: .success)
|
||||
return
|
||||
}
|
||||
request.value = (request.offset == 0 ? data : data.subdata(in: request.offset..<data.count))
|
||||
peripheral.respond(to: request, withResult: .success)
|
||||
}
|
||||
}
|
||||
default:
|
||||
logger.fault("Read (central=\(central.description),characteristic=unknown)")
|
||||
queue.async { peripheral.respond(to: request, withResult: .requestNotSupported) }
|
||||
}
|
||||
notifySubscribers("didReceiveRead")
|
||||
}
|
||||
|
||||
/// Another iOS central has subscribed to this iOS peripheral, implying the central is also a peripheral for this device to connect to.
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
|
||||
// Subscribe -> Notify subscribers
|
||||
// iOS receiver subscribes to the signal characteristic on first contact. This ensures the first call keeps
|
||||
// the transmitter and receiver awake. Future loops will rely on didReceiveWrite as the trigger.
|
||||
logger.debug("Subscribe (central=\(central.identifier.uuidString))")
|
||||
// FEATURE : Symmetric connection on subscribe
|
||||
_ = database.device(central.identifier.uuidString)
|
||||
notifySubscribers("didSubscribeTo")
|
||||
}
|
||||
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
|
||||
// Unsubscribe -> Notify subscribers
|
||||
logger.debug("Unsubscribe (central=\(central.identifier.uuidString))")
|
||||
// FEATURE : Symmetric connection on unsubscribe
|
||||
_ = database.device(central.identifier.uuidString)
|
||||
notifySubscribers("didUnsubscribeFrom")
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
/// Get Int8 from byte array (little-endian).
|
||||
func int8(_ index: Int) -> Int8? {
|
||||
guard let value = uint8(index) else {
|
||||
return nil
|
||||
}
|
||||
return Int8(bitPattern: value)
|
||||
}
|
||||
|
||||
/// Get UInt8 from byte array (little-endian).
|
||||
func uint8(_ index: Int) -> UInt8? {
|
||||
let bytes = [UInt8](self)
|
||||
guard index < bytes.count else {
|
||||
return nil
|
||||
}
|
||||
return bytes[index]
|
||||
}
|
||||
|
||||
/// Get Int16 from byte array (little-endian).
|
||||
func int16(_ index: Int) -> Int16? {
|
||||
guard let value = uint16(index) else {
|
||||
return nil
|
||||
}
|
||||
return Int16(bitPattern: value)
|
||||
}
|
||||
|
||||
/// Get UInt16 from byte array (little-endian).
|
||||
func uint16(_ index: Int) -> UInt16? {
|
||||
let bytes = [UInt8](self)
|
||||
guard index < (bytes.count - 1) else {
|
||||
return nil
|
||||
}
|
||||
return UInt16(bytes[index]) |
|
||||
UInt16(bytes[index + 1]) << 8
|
||||
}
|
||||
|
||||
/// Get Int32 from byte array (little-endian).
|
||||
func int32(_ index: Int) -> Int32? {
|
||||
guard let value = uint32(index) else {
|
||||
return nil
|
||||
}
|
||||
return Int32(bitPattern: value)
|
||||
}
|
||||
|
||||
/// Get UInt32 from byte array (little-endian).
|
||||
func uint32(_ index: Int) -> UInt32? {
|
||||
let bytes = [UInt8](self)
|
||||
guard index < (bytes.count - 3) else {
|
||||
return nil
|
||||
}
|
||||
return UInt32(bytes[index]) |
|
||||
UInt32(bytes[index + 1]) << 8 |
|
||||
UInt32(bytes[index + 2]) << 16 |
|
||||
UInt32(bytes[index + 3]) << 24
|
||||
}
|
||||
|
||||
/// Get Int64 from byte array (little-endian).
|
||||
func int64(_ index: Int) -> Int64? {
|
||||
guard let value = uint64(index) else {
|
||||
return nil
|
||||
}
|
||||
return Int64(bitPattern: value)
|
||||
}
|
||||
|
||||
/// Get UInt64 from byte array (little-endian).
|
||||
func uint64(_ index: Int) -> UInt64? {
|
||||
let bytes = [UInt8](self)
|
||||
guard index < (bytes.count - 7) else {
|
||||
return nil
|
||||
}
|
||||
return UInt64(bytes[index]) |
|
||||
UInt64(bytes[index + 1]) << 8 |
|
||||
UInt64(bytes[index + 2]) << 16 |
|
||||
UInt64(bytes[index + 3]) << 24 |
|
||||
UInt64(bytes[index + 4]) << 32 |
|
||||
UInt64(bytes[index + 5]) << 40 |
|
||||
UInt64(bytes[index + 6]) << 48 |
|
||||
UInt64(bytes[index + 7]) << 56
|
||||
}
|
||||
}
|
195
CovidSafe/Herald/Sensor/BLE/BLEUtilities.swift
Normal file
195
CovidSafe/Herald/Sensor/BLE/BLEUtilities.swift
Normal file
|
@ -0,0 +1,195 @@
|
|||
//
|
||||
// BLEUtilities.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
/**
|
||||
Extension to make the state human readable in logs.
|
||||
*/
|
||||
@available(iOS 10.0, *)
|
||||
extension CBManagerState: CustomStringConvertible {
|
||||
/**
|
||||
Get plain text description of state.
|
||||
*/
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .poweredOff: return ".poweredOff"
|
||||
case .poweredOn: return ".poweredOn"
|
||||
case .resetting: return ".resetting"
|
||||
case .unauthorized: return ".unauthorized"
|
||||
case .unknown: return ".unknown"
|
||||
case .unsupported: return ".unsupported"
|
||||
@unknown default: return "undefined"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CBPeripheralManagerState : CustomStringConvertible {
|
||||
/**
|
||||
Get plain text description of state.
|
||||
*/
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .poweredOff: return ".poweredOff"
|
||||
case .poweredOn: return ".poweredOn"
|
||||
case .resetting: return ".resetting"
|
||||
case .unauthorized: return ".unauthorized"
|
||||
case .unknown: return ".unknown"
|
||||
case .unsupported: return ".unsupported"
|
||||
@unknown default: return "undefined"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CBCentralManagerState : CustomStringConvertible {
|
||||
/**
|
||||
Get plain text description of state.
|
||||
*/
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .poweredOff: return ".poweredOff"
|
||||
case .poweredOn: return ".poweredOn"
|
||||
case .resetting: return ".resetting"
|
||||
case .unauthorized: return ".unauthorized"
|
||||
case .unknown: return ".unknown"
|
||||
case .unsupported: return ".unsupported"
|
||||
@unknown default: return "undefined"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Extension to make the state human readable in logs.
|
||||
*/
|
||||
extension CBPeripheralState: CustomStringConvertible {
|
||||
/**
|
||||
Get plain text description fo state.
|
||||
*/
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .connected: return ".connected"
|
||||
case .connecting: return ".connecting"
|
||||
case .disconnected: return ".disconnected"
|
||||
case .disconnecting: return ".disconnecting"
|
||||
@unknown default: return "undefined"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Extension to make the time intervals more human readable in code.
|
||||
*/
|
||||
extension TimeInterval {
|
||||
static var day: TimeInterval { get { TimeInterval(86400) } }
|
||||
static var hour: TimeInterval { get { TimeInterval(3600) } }
|
||||
static var minute: TimeInterval { get { TimeInterval(60) } }
|
||||
static var never: TimeInterval { get { TimeInterval(Int.max) } }
|
||||
}
|
||||
|
||||
/**
|
||||
Sample statistics.
|
||||
*/
|
||||
class Sample {
|
||||
private var n:Int64 = 0
|
||||
private var m1:Double = 0.0
|
||||
private var m2:Double = 0.0
|
||||
private var m3:Double = 0.0
|
||||
private var m4:Double = 0.0
|
||||
|
||||
/**
|
||||
Minimum sample value.
|
||||
*/
|
||||
var min:Double? = nil
|
||||
/**
|
||||
Maximum sample value.
|
||||
*/
|
||||
var max:Double? = nil
|
||||
/**
|
||||
Sample size.
|
||||
*/
|
||||
var count:Int64 { get { n } }
|
||||
/**
|
||||
Mean sample value.
|
||||
*/
|
||||
var mean:Double? { get { n > 0 ? m1 : nil } }
|
||||
/**
|
||||
Sample variance.
|
||||
*/
|
||||
var variance:Double? { get { n > 1 ? m2 / Double(n - 1) : nil } }
|
||||
/**
|
||||
Sample standard deviation.
|
||||
*/
|
||||
var standardDeviation:Double? { get { n > 1 ? sqrt(m2 / Double(n - 1)) : nil } }
|
||||
/**
|
||||
String representation of mean, standard deviation, min and max
|
||||
*/
|
||||
var description: String { get {
|
||||
let sCount = n.description
|
||||
let sMean = (mean == nil ? "-" : mean!.description)
|
||||
let sStandardDeviation = (standardDeviation == nil ? "-" : standardDeviation!.description)
|
||||
let sMin = (min == nil ? "-" : min!.description)
|
||||
let sMax = (max == nil ? "-" : max!.description)
|
||||
return "count=" + sCount + ",mean=" + sMean + ",sd=" + sStandardDeviation + ",min=" + sMin + ",max=" + sMax
|
||||
} }
|
||||
|
||||
/**
|
||||
Add sample value.
|
||||
*/
|
||||
func add(_ x:Double) {
|
||||
// Sample value accumulation algorithm avoids reiterating sample to compute variance.
|
||||
let n1 = n
|
||||
n += 1
|
||||
let d = x - m1
|
||||
let d_n = d / Double(n)
|
||||
let d_n2 = d_n * d_n;
|
||||
let t = d * d_n * Double(n1);
|
||||
m1 += d_n;
|
||||
m4 += t * d_n2 * Double(n * n - 3 * n + 3) + 6 * d_n2 * m2 - 4 * d_n * m3;
|
||||
m3 += t * d_n * Double(n - 2) - 3 * d_n * m2;
|
||||
m2 += t;
|
||||
if min == nil || x < min! {
|
||||
min = x;
|
||||
}
|
||||
if max == nil || x > max! {
|
||||
max = x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Time interval samples for collecting elapsed time statistics.
|
||||
*/
|
||||
class TimeIntervalSample : Sample {
|
||||
private var startTime: Date?
|
||||
private var timestamp: Date?
|
||||
var period: TimeInterval? { get {
|
||||
(startTime == nil ? nil : timestamp?.timeIntervalSince(startTime!))
|
||||
}}
|
||||
|
||||
override var description: String { get {
|
||||
let sPeriod = (period == nil ? "-" : period!.description)
|
||||
return super.description + ",period=" + sPeriod
|
||||
}}
|
||||
|
||||
/**
|
||||
Add elapsed time since last call to add() as sample.
|
||||
*/
|
||||
func add() {
|
||||
guard timestamp != nil else {
|
||||
timestamp = Date()
|
||||
startTime = timestamp
|
||||
return
|
||||
}
|
||||
let now = Date()
|
||||
if let timestamp = timestamp {
|
||||
add(now.timeIntervalSince(timestamp))
|
||||
}
|
||||
timestamp = now
|
||||
}
|
||||
}
|
50
CovidSafe/Herald/Sensor/Data/BatteryLog.swift
Normal file
50
CovidSafe/Herald/Sensor/Data/BatteryLog.swift
Normal file
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// BatteryLog.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import NotificationCenter
|
||||
import os
|
||||
|
||||
/// Battery log for monitoring battery level over time
|
||||
class BatteryLog {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "BatteryLog")
|
||||
private let textFile: TextFile
|
||||
private let dateFormatter = DateFormatter()
|
||||
private let updateInterval = TimeInterval(30)
|
||||
|
||||
init(filename: String) {
|
||||
textFile = TextFile(filename: filename)
|
||||
if textFile.empty() {
|
||||
textFile.write("time,source,level")
|
||||
}
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(batteryLevelDidChange), name: UIDevice.batteryLevelDidChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(batteryStateDidChange), name: UIDevice.batteryStateDidChangeNotification, object: nil)
|
||||
let _ = Timer.scheduledTimer(timeInterval: updateInterval, target: self, selector: #selector(update), userInfo: nil, repeats: true)
|
||||
}
|
||||
|
||||
private func timestamp() -> String {
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
return timestamp
|
||||
}
|
||||
|
||||
@objc func update() {
|
||||
let powerSource = (UIDevice.current.batteryState == .unplugged ? "battery" : "external")
|
||||
let batteryLevel = Float(UIDevice.current.batteryLevel * 100).description
|
||||
textFile.write(timestamp() + "," + powerSource + "," + batteryLevel)
|
||||
logger.debug("update (powerSource=\(powerSource),batteryLevel=\(batteryLevel))");
|
||||
}
|
||||
|
||||
@objc func batteryLevelDidChange(_ sender: NotificationCenter) {
|
||||
update()
|
||||
}
|
||||
|
||||
@objc func batteryStateDidChange(_ sender: NotificationCenter) {
|
||||
update()
|
||||
}
|
||||
}
|
57
CovidSafe/Herald/Sensor/Data/ContactLog.swift
Normal file
57
CovidSafe/Herald/Sensor/Data/ContactLog.swift
Normal file
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// ContactLog.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// CSV contact log for post event analysis and visualisation
|
||||
class ContactLog: NSObject, SensorDelegate {
|
||||
private let textFile: TextFile
|
||||
private let dateFormatter = DateFormatter()
|
||||
|
||||
init(filename: String) {
|
||||
textFile = TextFile(filename: filename)
|
||||
if textFile.empty() {
|
||||
textFile.write("time,sensor,id,detect,read,measure,share,visit,data")
|
||||
}
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
}
|
||||
|
||||
private func timestamp() -> String {
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
return timestamp
|
||||
}
|
||||
|
||||
private func csv(_ value: String) -> String {
|
||||
return TextFile.csv(value)
|
||||
}
|
||||
|
||||
// MARK:- SensorDelegate
|
||||
|
||||
func sensor(_ sensor: SensorType, didDetect: TargetIdentifier) {
|
||||
textFile.write(timestamp() + "," + sensor.rawValue + "," + csv(didDetect) + ",1,,,,,")
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier) {
|
||||
textFile.write(timestamp() + "," + sensor.rawValue + "," + csv(fromTarget) + ",,2,,,," + csv(didRead.shortName))
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier, atProximity: Proximity, withTxPower: Int?) {
|
||||
textFile.write(timestamp() + "," + sensor.rawValue + "," + csv(fromTarget) + ",,2,,,," + csv(didRead.shortName))
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier) {
|
||||
textFile.write(timestamp() + "," + sensor.rawValue + "," + csv(fromTarget) + ",,,3,,," + csv(didMeasure.description))
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier, atProximity: Proximity) {
|
||||
let prefix = timestamp() + "," + sensor.rawValue + "," + csv(fromTarget)
|
||||
didShare.forEach() { payloadData in
|
||||
textFile.write(prefix + ",,,,4,," + csv(payloadData.shortName))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
88
CovidSafe/Herald/Sensor/Data/DetectionLog.swift
Normal file
88
CovidSafe/Herald/Sensor/Data/DetectionLog.swift
Normal file
|
@ -0,0 +1,88 @@
|
|||
//
|
||||
// DetectionLog.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// CSV contact log for post event analysis and visualisation
|
||||
class DetectionLog: NSObject, SensorDelegate {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "Data.DetectionLog")
|
||||
private let textFile: TextFile
|
||||
private let payloadData: PayloadData
|
||||
private let deviceName = UIDevice.current.name
|
||||
private let deviceOS = UIDevice.current.systemVersion
|
||||
private var payloads: Set<String> = []
|
||||
private let queue = DispatchQueue(label: "Sensor.Data.DetectionLog.Queue")
|
||||
|
||||
init(filename: String, payloadData: PayloadData) {
|
||||
textFile = TextFile(filename: filename)
|
||||
self.payloadData = payloadData
|
||||
super.init()
|
||||
write()
|
||||
}
|
||||
|
||||
private func csv(_ value: String) -> String {
|
||||
return TextFile.csv(value)
|
||||
}
|
||||
|
||||
private func write() {
|
||||
var content = "\(csv(deviceName)),iOS,\(csv(deviceOS)),\(csv(payloadData.shortName))"
|
||||
var payloadList: [String] = []
|
||||
payloads.forEach() { payload in
|
||||
guard payload != payloadData.shortName else {
|
||||
return
|
||||
}
|
||||
payloadList.append(payload)
|
||||
}
|
||||
payloadList.sort()
|
||||
payloadList.forEach() { payload in
|
||||
content.append(",")
|
||||
content.append(csv(payload))
|
||||
}
|
||||
logger.debug("write (content=\(content))")
|
||||
content.append("\n")
|
||||
textFile.overwrite(content)
|
||||
}
|
||||
|
||||
// MARK:- SensorDelegate
|
||||
|
||||
func sensor(_ sensor: SensorType, didDetect: TargetIdentifier) {
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier) {
|
||||
queue.async {
|
||||
if self.payloads.insert(didRead.shortName).inserted {
|
||||
self.logger.debug("didRead (payload=\(didRead.shortName))")
|
||||
self.write()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier, atProximity: Proximity, withTxPower: Int?) {
|
||||
queue.async {
|
||||
if self.payloads.insert(didRead.shortName).inserted {
|
||||
self.logger.debug("didRead (payload=\(didRead.shortName))")
|
||||
self.write()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier) {
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier, atProximity: Proximity) {
|
||||
didShare.forEach() { payloadData in
|
||||
queue.async {
|
||||
if self.payloads.insert(payloadData.shortName).inserted {
|
||||
self.logger.debug("didShare (payload=\(payloadData.shortName))")
|
||||
self.write()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
96
CovidSafe/Herald/Sensor/Data/SensorLogger.swift
Normal file
96
CovidSafe/Herald/Sensor/Data/SensorLogger.swift
Normal file
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// Logger.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import os
|
||||
|
||||
protocol SensorLogger {
|
||||
init(subsystem: String, category: String)
|
||||
|
||||
func log(_ level: SensorLoggerLevel, _ message: String)
|
||||
|
||||
func debug(_ message: String)
|
||||
|
||||
func info(_ message: String)
|
||||
|
||||
func fault(_ message: String)
|
||||
}
|
||||
|
||||
enum SensorLoggerLevel: String {
|
||||
case debug, info, fault
|
||||
}
|
||||
|
||||
class ConcreteSensorLogger: NSObject, SensorLogger {
|
||||
private let subsystem: String
|
||||
private let category: String
|
||||
private let dateFormatter = DateFormatter()
|
||||
private let log: OSLog?
|
||||
private static let logFile = TextFile(filename: "log.txt")
|
||||
|
||||
required init(subsystem: String, category: String) {
|
||||
self.subsystem = subsystem
|
||||
self.category = category
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
if #available(iOS 10.0, *) {
|
||||
log = OSLog(subsystem: subsystem, category: category)
|
||||
} else {
|
||||
log = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func suppress(_ level: SensorLoggerLevel) -> Bool {
|
||||
switch level {
|
||||
case .debug:
|
||||
return (BLESensorConfiguration.logLevel == .info || BLESensorConfiguration.logLevel == .fault);
|
||||
case .info:
|
||||
return (BLESensorConfiguration.logLevel == .fault);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
func log(_ level: SensorLoggerLevel, _ message: String) {
|
||||
guard !suppress(level) else {
|
||||
return
|
||||
}
|
||||
// Write to unified os log if available, else print to console
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
let csvMessage = message.replacingOccurrences(of: "\"", with: "'")
|
||||
let quotedMessage = (message.contains(",") ? "\"" + csvMessage + "\"" : csvMessage)
|
||||
let entry = timestamp + "," + level.rawValue + "," + subsystem + "," + category + "," + quotedMessage
|
||||
ConcreteSensorLogger.logFile.write(entry)
|
||||
guard let log = log else {
|
||||
print(entry)
|
||||
return
|
||||
}
|
||||
if #available(iOS 10.0, *) {
|
||||
switch (level) {
|
||||
case .debug:
|
||||
os_log("%s", log: log, type: .debug, message)
|
||||
case .info:
|
||||
os_log("%s", log: log, type: .info, message)
|
||||
case .fault:
|
||||
os_log("%s", log: log, type: .fault, message)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func debug(_ message: String) {
|
||||
log(.debug, message)
|
||||
}
|
||||
|
||||
func info(_ message: String) {
|
||||
log(.debug, message)
|
||||
}
|
||||
|
||||
func fault(_ message: String) {
|
||||
log(.debug, message)
|
||||
}
|
||||
|
||||
}
|
95
CovidSafe/Herald/Sensor/Data/StatisticsLog.swift
Normal file
95
CovidSafe/Herald/Sensor/Data/StatisticsLog.swift
Normal file
|
@ -0,0 +1,95 @@
|
|||
//
|
||||
// StatisticsLog.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// CSV contact log for post event analysis and visualisation
|
||||
class StatisticsLog: NSObject, SensorDelegate {
|
||||
private let textFile: TextFile
|
||||
private let payloadData: PayloadData
|
||||
private var identifierToPayload: [TargetIdentifier:String] = [:]
|
||||
private var payloadToTime: [String:Date] = [:]
|
||||
private var payloadToSample: [String:Sample] = [:]
|
||||
|
||||
init(filename: String, payloadData: PayloadData) {
|
||||
textFile = TextFile(filename: filename)
|
||||
self.payloadData = payloadData
|
||||
}
|
||||
|
||||
private func csv(_ value: String) -> String {
|
||||
return TextFile.csv(value)
|
||||
}
|
||||
|
||||
private func add(identifier: TargetIdentifier) {
|
||||
guard let payload = identifierToPayload[identifier] else {
|
||||
return
|
||||
}
|
||||
add(payload: payload)
|
||||
}
|
||||
|
||||
private func add(payload: String) {
|
||||
guard let time = payloadToTime[payload], let sample = payloadToSample[payload] else {
|
||||
payloadToTime[payload] = Date()
|
||||
payloadToSample[payload] = Sample()
|
||||
return
|
||||
}
|
||||
let now = Date()
|
||||
payloadToTime[payload] = now
|
||||
sample.add(Double(now.timeIntervalSince(time)))
|
||||
write()
|
||||
}
|
||||
|
||||
private func write() {
|
||||
var content = "payload,count,mean,sd,min,max\n"
|
||||
var payloadList: [String] = []
|
||||
payloadToSample.keys.forEach() { payload in
|
||||
guard payload != payloadData.shortName else {
|
||||
return
|
||||
}
|
||||
payloadList.append(payload)
|
||||
}
|
||||
payloadList.sort()
|
||||
payloadList.forEach() { payload in
|
||||
guard let sample = payloadToSample[payload] else {
|
||||
return
|
||||
}
|
||||
guard let mean = sample.mean, let sd = sample.standardDeviation, let min = sample.min, let max = sample.max else {
|
||||
return
|
||||
}
|
||||
content.append("\(csv(payload)),\(sample.count),\(mean),\(sd),\(min),\(max)\n")
|
||||
}
|
||||
textFile.overwrite(content)
|
||||
}
|
||||
|
||||
|
||||
// MARK:- SensorDelegate
|
||||
|
||||
func sensor(_ sensor: SensorType, didDetect: TargetIdentifier) {
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier) {
|
||||
identifierToPayload[fromTarget] = didRead.shortName
|
||||
add(identifier: fromTarget)
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier, atProximity: Proximity, withTxPower: Int?) {
|
||||
identifierToPayload[fromTarget] = didRead.shortName
|
||||
add(identifier: fromTarget)
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier) {
|
||||
add(identifier: fromTarget)
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier, atProximity: Proximity) {
|
||||
didShare.forEach() { payloadData in
|
||||
add(payload: payloadData.shortName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
71
CovidSafe/Herald/Sensor/Data/TextFile.swift
Normal file
71
CovidSafe/Herald/Sensor/Data/TextFile.swift
Normal file
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// TextFile.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class TextFile {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "Data.TextFile")
|
||||
private var file: URL?
|
||||
private let queue: DispatchQueue
|
||||
|
||||
init(filename: String) {
|
||||
file = try? FileManager.default
|
||||
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
.appendingPathComponent(filename)
|
||||
queue = DispatchQueue(label: "Sensor.Data.TextFile(\(filename))")
|
||||
}
|
||||
|
||||
func empty() -> Bool {
|
||||
guard let file = file else {
|
||||
return true
|
||||
}
|
||||
return !FileManager.default.fileExists(atPath: file.path)
|
||||
}
|
||||
|
||||
/// Append line to new or existing file
|
||||
func write(_ line: String) {
|
||||
queue.sync {
|
||||
guard let file = file else {
|
||||
return
|
||||
}
|
||||
guard let data = (line+"\n").data(using: .utf8) else {
|
||||
return
|
||||
}
|
||||
if FileManager.default.fileExists(atPath: file.path) {
|
||||
if let fileHandle = try? FileHandle(forWritingTo: file) {
|
||||
fileHandle.seekToEndOfFile()
|
||||
fileHandle.write(data)
|
||||
fileHandle.closeFile()
|
||||
}
|
||||
} else {
|
||||
try? data.write(to: file, options: .atomicWrite)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Overwrite file content
|
||||
func overwrite(_ content: String) {
|
||||
queue.sync {
|
||||
guard let file = file else {
|
||||
return
|
||||
}
|
||||
guard let data = content.data(using: .utf8) else {
|
||||
return
|
||||
}
|
||||
try? data.write(to: file, options: .atomicWrite)
|
||||
}
|
||||
}
|
||||
|
||||
/// Quote value for CSV output if required.
|
||||
static func csv(_ value: String) -> String {
|
||||
guard value.contains(",") || value.contains("\"") || value.contains("'") || value.contains("’") else {
|
||||
return value
|
||||
}
|
||||
return "\"" + value + "\""
|
||||
|
||||
}
|
||||
}
|
122
CovidSafe/Herald/Sensor/Location/AwakeSensor.swift
Normal file
122
CovidSafe/Herald/Sensor/Location/AwakeSensor.swift
Normal file
|
@ -0,0 +1,122 @@
|
|||
//
|
||||
// AwakeSensor.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
|
||||
protocol AwakeSensor : Sensor {
|
||||
}
|
||||
|
||||
/**
|
||||
Screen awake sensor based on CoreLocation. Does NOT make use of the GPS position
|
||||
Requires : Signing & Capabilities : BackgroundModes : LocationUpdates = YES
|
||||
Requires : Info.plist : Privacy - Location When In Use Usage Description
|
||||
Requires : Info.plist : Privacy - Location Always and When In Use Usage Description
|
||||
*/
|
||||
class ConcreteAwakeSensor : NSObject, AwakeSensor, CLLocationManagerDelegate {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "ConcreteAwakeSensor")
|
||||
private var delegates: [SensorDelegate] = []
|
||||
private let locationManager = CLLocationManager()
|
||||
private let rangeForBeacon: UUID?
|
||||
|
||||
init(desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyThreeKilometers, distanceFilter: CLLocationDistance = CLLocationDistanceMax, rangeForBeacon: UUID? = nil) {
|
||||
logger.debug("init(desiredAccuracy=\(desiredAccuracy == kCLLocationAccuracyThreeKilometers ? "3km" : desiredAccuracy.description),distanceFilter=\(distanceFilter == CLLocationDistanceMax ? "max" : distanceFilter.description),rangeForBeacon=\(rangeForBeacon == nil ? "disabled" : rangeForBeacon!.description))")
|
||||
self.rangeForBeacon = rangeForBeacon
|
||||
super.init()
|
||||
locationManager.delegate = self
|
||||
locationManager.requestAlwaysAuthorization()
|
||||
locationManager.pausesLocationUpdatesAutomatically = false
|
||||
locationManager.desiredAccuracy = desiredAccuracy
|
||||
locationManager.distanceFilter = distanceFilter
|
||||
locationManager.allowsBackgroundLocationUpdates = true
|
||||
if #available(iOS 11.0, *) {
|
||||
logger.debug("init(ios>=11.0)")
|
||||
locationManager.showsBackgroundLocationIndicator = false
|
||||
} else {
|
||||
logger.debug("init(ios<11.0)")
|
||||
}
|
||||
}
|
||||
|
||||
func add(delegate: SensorDelegate) {
|
||||
delegates.append(delegate)
|
||||
}
|
||||
|
||||
func start() {
|
||||
logger.debug("start")
|
||||
locationManager.startUpdatingLocation()
|
||||
logger.debug("startUpdatingLocation")
|
||||
|
||||
// Start beacon ranging
|
||||
guard let beaconUUID = rangeForBeacon else {
|
||||
return
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
locationManager.startRangingBeacons(satisfying: CLBeaconIdentityConstraint(uuid: beaconUUID))
|
||||
logger.debug("startRangingBeacons(ios>=13.0,beaconUUID=\(beaconUUID.description))")
|
||||
} else {
|
||||
let beaconRegion = CLBeaconRegion(proximityUUID: beaconUUID, identifier: beaconUUID.uuidString)
|
||||
locationManager.startRangingBeacons(in: beaconRegion)
|
||||
logger.debug("startRangingBeacons(ios<13.0,beaconUUID=\(beaconUUID.uuidString)))")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
func stop() {
|
||||
logger.debug("stop")
|
||||
locationManager.stopUpdatingLocation()
|
||||
logger.debug("stopUpdatingLocation")
|
||||
// Start beacon ranging
|
||||
guard let beaconUUID = rangeForBeacon else {
|
||||
return
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
locationManager.stopRangingBeacons(satisfying: CLBeaconIdentityConstraint(uuid: beaconUUID))
|
||||
logger.debug("stopRangingBeacons(ios>=13.0,beaconUUID=\(beaconUUID.description))")
|
||||
} else {
|
||||
let beaconRegion = CLBeaconRegion(proximityUUID: beaconUUID, identifier: beaconUUID.uuidString)
|
||||
locationManager.stopRangingBeacons(in: beaconRegion)
|
||||
logger.debug("stopRangingBeacons(ios<13.0,beaconUUID=\(beaconUUID.description))")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:- CLLocationManagerDelegate
|
||||
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
|
||||
var state = SensorState.off
|
||||
|
||||
if status == CLAuthorizationStatus.authorizedWhenInUse ||
|
||||
status == CLAuthorizationStatus.authorizedAlways {
|
||||
state = .on
|
||||
}
|
||||
if status == CLAuthorizationStatus.notDetermined {
|
||||
locationManager.requestAlwaysAuthorization()
|
||||
locationManager.stopUpdatingLocation()
|
||||
locationManager.startUpdatingLocation()
|
||||
}
|
||||
if status != CLAuthorizationStatus.notDetermined {
|
||||
delegates.forEach({ $0.sensor(.AWAKE, didUpdateState: state) })
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
var state = SensorState.off
|
||||
if manager.authorizationStatus == CLAuthorizationStatus.authorizedWhenInUse ||
|
||||
manager.authorizationStatus == CLAuthorizationStatus.authorizedAlways {
|
||||
state = .on
|
||||
}
|
||||
if manager.authorizationStatus == CLAuthorizationStatus.notDetermined {
|
||||
locationManager.requestAlwaysAuthorization()
|
||||
locationManager.stopUpdatingLocation()
|
||||
locationManager.startUpdatingLocation()
|
||||
}
|
||||
if manager.authorizationStatus != CLAuthorizationStatus.notDetermined {
|
||||
delegates.forEach({ $0.sensor(.AWAKE, didUpdateState: state) })
|
||||
}
|
||||
}
|
||||
|
||||
}
|
53
CovidSafe/Herald/Sensor/PayloadDataSupplier.swift
Normal file
53
CovidSafe/Herald/Sensor/PayloadDataSupplier.swift
Normal file
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// PayloadDataSupplier.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Payload data supplier for generating payload data that is shared with other devices to provide device identity information while maintaining privacy and security.
|
||||
/// Implement this to integration your solution with this transport.
|
||||
public protocol PayloadDataSupplier {
|
||||
/// Get payload for given timestamp. Use this for integration with any payload generator.
|
||||
func payload(_ timestamp: PayloadTimestamp) -> PayloadData
|
||||
|
||||
/// Get payload for given identifier. Use this for integration with any payload generator.
|
||||
func payload(_ identifier: UUID, offset: Int, onComplete: @escaping (PayloadData?) -> Void) -> Void
|
||||
|
||||
/// Parse raw data into payloads. This is used to split concatenated payloads that are transmitted via share payload. The default implementation assumes payload data is fixed length.
|
||||
func payload(_ data: Data) -> [PayloadData]
|
||||
}
|
||||
|
||||
/// Implements payload splitting function, assuming fixed length payloads.
|
||||
public extension PayloadDataSupplier {
|
||||
/// Default implementation assumes fixed length payload data.
|
||||
func payload(_ data: Data) -> [PayloadData] {
|
||||
// Get example payload to determine length
|
||||
let fixedLengthPayload = payload(PayloadTimestamp())
|
||||
let payloadLength = fixedLengthPayload.count
|
||||
// Split data into payloads based on fixed length
|
||||
var payloads: [PayloadData] = []
|
||||
var indexStart = 0, indexEnd = payloadLength
|
||||
while indexEnd <= data.count {
|
||||
let payload = PayloadData(data.subdata(in: indexStart..<indexEnd))
|
||||
payloads.append(payload)
|
||||
indexStart += payloadLength
|
||||
indexEnd += payloadLength
|
||||
}
|
||||
return payloads
|
||||
}
|
||||
|
||||
/// Default Implementation returns payload(timestamp:)
|
||||
func payload(_ identifier: UUID, offset: Int, onComplete: @escaping (PayloadData?) -> Void) -> Void {
|
||||
onComplete(payload(PayloadTimestamp()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Payload timestamp, should normally be Date, but it may change to UInt64 in the future to use server synchronised relative timestamp.
|
||||
public typealias PayloadTimestamp = Date
|
||||
|
||||
/// Encrypted payload data received from target. This is likely to be an encrypted datagram of the target's actual permanent identifier.
|
||||
public typealias PayloadData = Data
|
||||
|
21
CovidSafe/Herald/Sensor/Sensor.swift
Normal file
21
CovidSafe/Herald/Sensor/Sensor.swift
Normal file
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// Sensor.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Sensor for detecting and tracking various kinds of disease transmission vectors, e.g. contact with people, time at location.
|
||||
public protocol Sensor {
|
||||
/// Add delegate for responding to sensor events.
|
||||
func add(delegate: SensorDelegate)
|
||||
|
||||
/// Start sensing.
|
||||
func start()
|
||||
|
||||
/// Stop sensing.
|
||||
func stop()
|
||||
}
|
||||
|
59
CovidSafe/Herald/Sensor/SensorArray.swift
Normal file
59
CovidSafe/Herald/Sensor/SensorArray.swift
Normal file
|
@ -0,0 +1,59 @@
|
|||
//
|
||||
// SensorArray.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// Sensor array for combining multiple detection and tracking methods.
|
||||
public class SensorArray : NSObject, Sensor {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "SensorArray")
|
||||
private var sensorArray: [Sensor] = []
|
||||
private var sensorDelegates: [SensorDelegate] = []
|
||||
public let payloadData: PayloadData
|
||||
public static let deviceDescription = "\(UIDevice.current.name) (iOS \(UIDevice.current.systemVersion))"
|
||||
|
||||
public init(_ payloadDataSupplier: PayloadDataSupplier) {
|
||||
logger.debug("init")
|
||||
// BLE sensor for detecting and tracking proximity
|
||||
sensorArray.append(ConcreteBLESensor(payloadDataSupplier))
|
||||
// Payload data at initiation time for identifying this device in the logs
|
||||
payloadData = payloadDataSupplier.payload(PayloadTimestamp())
|
||||
super.init()
|
||||
|
||||
// Loggers
|
||||
#if DEBUG
|
||||
add(delegate: ContactLog(filename: "contacts.csv"))
|
||||
add(delegate: StatisticsLog(filename: "statistics.csv", payloadData: payloadData))
|
||||
add(delegate: DetectionLog(filename: "detection.csv", payloadData: payloadData))
|
||||
_ = BatteryLog(filename: "battery.csv")
|
||||
#endif
|
||||
logger.info("DEVICE (payloadPrefix=\(payloadData.shortName),description=\(SensorArray.deviceDescription))")
|
||||
}
|
||||
|
||||
public func add(delegate: SensorDelegate) {
|
||||
sensorDelegates.append(delegate)
|
||||
sensorArray.forEach { $0.add(delegate: delegate) }
|
||||
}
|
||||
|
||||
public func start() {
|
||||
logger.debug("start")
|
||||
sensorArray.forEach { $0.start() }
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
logger.debug("stop")
|
||||
sensorArray.forEach { $0.stop() }
|
||||
}
|
||||
|
||||
public func startAwakeSensor() {
|
||||
// Location sensor is necessary for enabling background BLE advert detection
|
||||
let awakeSensor = ConcreteAwakeSensor(rangeForBeacon: UUID(uuidString: BLESensorConfiguration.serviceUUID.uuidString))
|
||||
sensorDelegates.forEach { awakeSensor.add(delegate: $0) }
|
||||
sensorArray.append(awakeSensor)
|
||||
awakeSensor.start()
|
||||
}
|
||||
}
|
100
CovidSafe/Herald/Sensor/SensorDelegate.swift
Normal file
100
CovidSafe/Herald/Sensor/SensorDelegate.swift
Normal file
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// SensorDelegate.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Sensor delegate for receiving sensor events.
|
||||
public protocol SensorDelegate {
|
||||
/// Detection of a target with an ephemeral identifier, e.g. BLE central detecting a BLE peripheral.
|
||||
func sensor(_ sensor: SensorType, didDetect: TargetIdentifier)
|
||||
|
||||
/// Read payload data from target, e.g. encrypted device identifier from BLE peripheral after successful connection.
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier)
|
||||
|
||||
/// Read payload data of other targets recently acquired by a target, e.g. Android peripheral sharing payload data acquired from nearby iOS peripherals.
|
||||
func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier, atProximity: Proximity)
|
||||
|
||||
/// Measure proximity to target, e.g. a sample of RSSI values from BLE peripheral.
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier)
|
||||
|
||||
/// Measure proximity to target with payload data. Combines didMeasure and didRead into a single convenient delegate method
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier, withPayload: PayloadData)
|
||||
|
||||
/// Measure proximity to target with payload data. Combines didMeasure and didRead into a single convenient delegate method
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier, withPayload: PayloadData, forDevice: BLEDevice)
|
||||
|
||||
/// Measure proximity to target with payload data. Combines didMeasure and didRead into a single convenient delegate method
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier, atProximity: Proximity, withTxPower: Int?)
|
||||
|
||||
/// Sensor state update
|
||||
func sensor(_ sensor: SensorType, didUpdateState: SensorState)
|
||||
}
|
||||
|
||||
/// Sensor delegate functions are all optional.
|
||||
public extension SensorDelegate {
|
||||
func sensor(_ sensor: SensorType, didDetect: TargetIdentifier) {}
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier) {}
|
||||
func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier, atProximity: Proximity) {}
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier) {}
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier, withPayload: PayloadData) {}
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier, withPayload: PayloadData, forDevice: BLEDevice) {}
|
||||
func sensor(_ sensor: SensorType, didUpdateState: SensorState) {}
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier, atProximity: Proximity, withTxPower: Int?) {}
|
||||
}
|
||||
|
||||
// MARK:- SensorDelegate data
|
||||
|
||||
/// Sensor type as qualifier for target identifier.
|
||||
public enum SensorType : String {
|
||||
/// Bluetooth Low Energy (BLE)
|
||||
case BLE
|
||||
/// Awake location sensor - uses Location API to be alerted to screen on events
|
||||
case AWAKE
|
||||
/// GPS location sensor - not used by default in Herald
|
||||
case GPS
|
||||
/// Physical beacon, e.g. iBeacon
|
||||
case BEACON
|
||||
/// Ultrasound audio beacon.
|
||||
case ULTRASOUND
|
||||
}
|
||||
|
||||
/// Sensor state
|
||||
public enum SensorState : String {
|
||||
/// Sensor is powered on, active and operational
|
||||
case on
|
||||
/// Sensor is powered off, inactive and not operational
|
||||
case off
|
||||
/// Sensor is not available
|
||||
case unavailable
|
||||
}
|
||||
|
||||
/// Ephemeral identifier for detected target (e.g. smartphone, beacon, place). This is likely to be an UUID but using String for variable identifier length.
|
||||
public typealias TargetIdentifier = String
|
||||
|
||||
// MARK:- Proximity data
|
||||
|
||||
/// Raw data for estimating proximity between sensor and target, e.g. RSSI for BLE.
|
||||
public struct Proximity {
|
||||
/// Unit of measurement, e.g. RSSI
|
||||
let unit: ProximityMeasurementUnit
|
||||
/// Measured value, e.g. raw RSSI value.
|
||||
let value: Double
|
||||
/// Get plain text description of proximity data
|
||||
public var description: String { get {
|
||||
unit.rawValue + ":" + value.description
|
||||
}}
|
||||
}
|
||||
|
||||
/// Measurement unit for interpreting the proximity data values.
|
||||
public enum ProximityMeasurementUnit : String {
|
||||
/// Received signal strength indicator, e.g. BLE signal strength as proximity estimator.
|
||||
case RSSI
|
||||
/// Roundtrip time, e.g. Audio signal echo time duration as proximity estimator.
|
||||
case RTT
|
||||
}
|
||||
|
||||
|
18
CovidSafe/Herald/herald.h
Normal file
18
CovidSafe/Herald/herald.h
Normal file
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// Herald.h
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for Herald.
|
||||
FOUNDATION_EXPORT double HeraldVersionNumber;
|
||||
|
||||
//! Project version string for Herald.
|
||||
FOUNDATION_EXPORT const unsigned char HeraldVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <Herald/PublicHeader.h>
|
||||
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
|
@ -23,6 +23,7 @@
|
|||
<outlet property="inactiveAppSectionView" destination="784-Jf-kOX" id="J3m-Pu-697"/>
|
||||
<outlet property="inactiveSettingsContent" destination="AUW-C2-ven" id="NNP-o9-zkK"/>
|
||||
<outlet property="inactiveTokenExpiredView" destination="nxM-ji-ttb" id="vVC-KW-yek"/>
|
||||
<outlet property="locationPermissionsView" destination="u4f-uR-ri3" id="70q-Jr-1cc"/>
|
||||
<outlet property="pairingRequestsLabel" destination="EF2-ER-Q6h" id="Wx5-bg-Rp3"/>
|
||||
<outlet property="scrollView" destination="Lws-gT-pWp" id="pTG-Kh-B9T"/>
|
||||
<outlet property="shareView" destination="vs9-rS-UOM" id="eoj-sn-TkG"/>
|
||||
|
@ -42,6 +43,11 @@
|
|||
<action selector="onAppSettingsTapped:" destination="-1" id="Tiq-zP-NGf"/>
|
||||
</connections>
|
||||
</tapGestureRecognizer>
|
||||
<tapGestureRecognizer id="yhN-Oh-SVH" userLabel="LocationAppSettingsTapped">
|
||||
<connections>
|
||||
<action selector="onAppSettingsTapped:" destination="-1" id="guC-1O-5d5"/>
|
||||
</connections>
|
||||
</tapGestureRecognizer>
|
||||
<tapGestureRecognizer id="eUy-DU-cZn" userLabel="BluetoothPairingLabelTapped">
|
||||
<connections>
|
||||
<action selector="bluetoothPairingTapped:" destination="-1" id="byI-uk-ZfC"/>
|
||||
|
@ -165,19 +171,19 @@
|
|||
</constraints>
|
||||
</view>
|
||||
<stackView opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="h36-8c-K2n">
|
||||
<rect key="frame" x="0.0" y="120" width="414" height="1566.5"/>
|
||||
<rect key="frame" x="0.0" y="120" width="414" height="1711"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bRs-XW-qzv" userLabel="StatusView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="764"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="908.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="rcS-nL-IAO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="752"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896.5"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="784-Jf-kOX" userLabel="InactiveView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="552"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="696.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="1" translatesAutoresizingMaskIntoConstraints="NO" id="xti-6W-zko" userLabel="Inactive Stack View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="552"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="696.5"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="4fe-SU-8Q6" userLabel="InActiveHeader">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="81"/>
|
||||
|
@ -367,7 +373,7 @@
|
|||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ers-f9-BFH" userLabel="Bluetooth Status Off Section">
|
||||
<rect key="frame" x="0.0" y="450.5" width="414" height="101.5"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="p5d-dk-foR" userLabel="Bluetooth Status Bar Off">
|
||||
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="p5d-dk-foR" userLabel="Bluetooth Status Bar Off">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="101.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Bluetooth®: Off" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="g0P-dF-xcR">
|
||||
|
@ -431,6 +437,74 @@
|
|||
<outletCollection property="gestureRecognizers" destination="tGp-em-x7B" appends="YES" id="mo7-ga-Hnw"/>
|
||||
</connections>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" verticalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="u4f-uR-ri3" userLabel="Location Settings">
|
||||
<rect key="frame" x="0.0" y="553" width="414" height="143.5"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="aGh-Fm-LNa" userLabel="Location Off bar">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="142.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Location settings" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4a2-uh-JwT">
|
||||
<rect key="frame" x="16" y="16" width="150" height="24"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle3"/>
|
||||
<color key="textColor" red="0.63921568630000003" green="0.098039215690000001" blue="0.098039215690000001" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="location_off"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<button opaque="NO" userInteractionEnabled="NO" contentMode="center" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GjW-bP-Gyh">
|
||||
<rect key="frame" x="364" y="6" width="44" height="44"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<bool key="isElement" value="NO"/>
|
||||
</accessibility>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="TWO-o0-rOr"/>
|
||||
<constraint firstAttribute="width" constant="44" id="tPO-9M-U8r"/>
|
||||
</constraints>
|
||||
<state key="normal" image="chevron-right-red"/>
|
||||
</button>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6HO-2o-TTh">
|
||||
<rect key="frame" x="16" y="44" width="340" height="82.5"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<bool key="isElement" value="NO"/>
|
||||
</accessibility>
|
||||
<string key="text">Enabling Location Services improves Bluetooth performance when identifying close contacts. COVIDSafe does NOT store or use location data.</string>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
|
||||
<color key="textColor" red="0.63921568630000003" green="0.098039215690000001" blue="0.098039215690000001" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="location_off_description"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="6HO-2o-TTh" secondAttribute="bottom" constant="16" id="2bJ-8i-mJK"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="4a2-uh-JwT" secondAttribute="trailing" constant="20" symbolic="YES" id="ABg-mm-FYN"/>
|
||||
<constraint firstItem="4a2-uh-JwT" firstAttribute="top" secondItem="aGh-Fm-LNa" secondAttribute="top" constant="16" id="Cil-S8-xIK"/>
|
||||
<constraint firstAttribute="trailing" secondItem="GjW-bP-Gyh" secondAttribute="trailing" constant="6" id="IuY-4n-l2D"/>
|
||||
<constraint firstItem="6HO-2o-TTh" firstAttribute="leading" secondItem="aGh-Fm-LNa" secondAttribute="leading" constant="16" id="Jv8-9f-6YY"/>
|
||||
<constraint firstItem="4a2-uh-JwT" firstAttribute="leading" secondItem="aGh-Fm-LNa" secondAttribute="leading" constant="16" id="LdD-zr-19B"/>
|
||||
<constraint firstItem="GjW-bP-Gyh" firstAttribute="leading" secondItem="6HO-2o-TTh" secondAttribute="trailing" constant="8" id="MTX-4f-BX4"/>
|
||||
<constraint firstItem="GjW-bP-Gyh" firstAttribute="centerY" secondItem="4a2-uh-JwT" secondAttribute="centerY" id="g01-mZ-oty"/>
|
||||
<constraint firstItem="6HO-2o-TTh" firstAttribute="top" secondItem="4a2-uh-JwT" secondAttribute="bottom" constant="4" id="wR8-8y-108"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<accessibility key="accessibilityConfiguration" label="Bluetooth permissions off">
|
||||
<accessibilityTraits key="traits" button="YES"/>
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
<constraints>
|
||||
<constraint firstItem="aGh-Fm-LNa" firstAttribute="top" secondItem="u4f-uR-ri3" secondAttribute="top" id="6KK-e5-iql"/>
|
||||
<constraint firstAttribute="bottom" secondItem="aGh-Fm-LNa" secondAttribute="bottom" constant="1" id="Qne-bx-T9S"/>
|
||||
<constraint firstItem="aGh-Fm-LNa" firstAttribute="leading" secondItem="u4f-uR-ri3" secondAttribute="leading" id="ZbU-z8-9iT"/>
|
||||
<constraint firstAttribute="trailing" secondItem="aGh-Fm-LNa" secondAttribute="trailing" id="bgp-cU-6fm"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="yhN-Oh-SVH" appends="YES" id="uuY-YZ-hRu"/>
|
||||
</connections>
|
||||
</view>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
|
@ -443,7 +517,7 @@
|
|||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="m1D-93-7sF" userLabel="ActiveView">
|
||||
<rect key="frame" x="0.0" y="552" width="414" height="200"/>
|
||||
<rect key="frame" x="0.0" y="696.5" width="414" height="200"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="1" translatesAutoresizingMaskIntoConstraints="NO" id="981-Cd-Xm3" userLabel="Active Stack View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="200"/>
|
||||
|
@ -562,7 +636,7 @@
|
|||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Aop-Ae-hRv" userLabel="StatisticsViewSection">
|
||||
<rect key="frame" x="0.0" y="764" width="414" height="124"/>
|
||||
<rect key="frame" x="0.0" y="908.5" width="414" height="124"/>
|
||||
<subviews>
|
||||
<view clipsSubviews="YES" contentMode="scaleToFill" verticalHuggingPriority="1" translatesAutoresizingMaskIntoConstraints="NO" id="eZl-C5-gSv" userLabel="StatisticsView">
|
||||
<rect key="frame" x="0.0" y="12" width="414" height="100"/>
|
||||
|
@ -580,7 +654,7 @@
|
|||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9cE-NC-A20" userLabel="Help">
|
||||
<rect key="frame" x="0.0" y="888" width="414" height="153"/>
|
||||
<rect key="frame" x="0.0" y="1032.5" width="414" height="153"/>
|
||||
<subviews>
|
||||
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="co1-dK-1WU">
|
||||
<rect key="frame" x="0.0" y="12" width="414" height="129"/>
|
||||
|
@ -713,7 +787,7 @@
|
|||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7cN-DY-lc3" userLabel="Change language">
|
||||
<rect key="frame" x="0.0" y="1041" width="414" height="153"/>
|
||||
<rect key="frame" x="0.0" y="1185.5" width="414" height="153"/>
|
||||
<subviews>
|
||||
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bE9-gT-Hba">
|
||||
<rect key="frame" x="0.0" y="12" width="414" height="129"/>
|
||||
|
@ -846,7 +920,7 @@
|
|||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vs9-rS-UOM" userLabel="Share CovidSafe">
|
||||
<rect key="frame" x="0.0" y="1194" width="414" height="151"/>
|
||||
<rect key="frame" x="0.0" y="1338.5" width="414" height="151"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RCa-zU-3Vo">
|
||||
<rect key="frame" x="0.0" y="12" width="414" height="127"/>
|
||||
|
@ -957,7 +1031,7 @@
|
|||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="JSe-D6-hyV" userLabel="Upload Data">
|
||||
<rect key="frame" x="0.0" y="1345" width="414" height="165.5"/>
|
||||
<rect key="frame" x="0.0" y="1489.5" width="414" height="165.5"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8pS-Df-p0U">
|
||||
<rect key="frame" x="0.0" y="12" width="414" height="141.5"/>
|
||||
|
@ -1077,7 +1151,7 @@
|
|||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="eZ2-CQ-dtQ" userLabel="Version View">
|
||||
<rect key="frame" x="0.0" y="1510.5" width="414" height="56"/>
|
||||
<rect key="frame" x="0.0" y="1655" width="414" height="56"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Version number:" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="O1w-Sl-OIH" userLabel="Version">
|
||||
<rect key="frame" x="16" y="12" width="382" height="20"/>
|
||||
|
|
|
@ -10,6 +10,7 @@ class HomeViewController: UIViewController {
|
|||
|
||||
@IBOutlet weak var bluetoothStatusOffView: UIView!
|
||||
@IBOutlet weak var bluetoothPermissionOffView: UIView!
|
||||
@IBOutlet weak var locationPermissionsView: UIView!
|
||||
@IBOutlet weak var inactiveSettingsContent: UIView!
|
||||
@IBOutlet weak var inactiveTokenExpiredView: UIView!
|
||||
@IBOutlet weak var shareView: UIView!
|
||||
|
@ -48,6 +49,7 @@ class HomeViewController: UIViewController {
|
|||
var bluetoothStatusOn = true
|
||||
var bluetoothPermissionOn = true
|
||||
var pushNotificationOn = true
|
||||
var locationPermissionOn = true
|
||||
var shouldShowUpdateApp = false
|
||||
|
||||
var didUploadData: Bool {
|
||||
|
@ -100,15 +102,18 @@ class HomeViewController: UIViewController {
|
|||
scrollView.refreshControl = UIRefreshControl()
|
||||
scrollView.refreshControl?.addTarget(self, action: #selector(refreshControlEvent), for: .valueChanged)
|
||||
|
||||
|
||||
// this is to show the settings prompt initially if bluetooth is off
|
||||
if !BluetraceManager.shared.isBluetoothOn() {
|
||||
BluetraceManager.shared.turnOn()
|
||||
}
|
||||
|
||||
observer = NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [unowned self] notification in
|
||||
self.refreshView()
|
||||
}
|
||||
BluetraceManager.shared.sensorDidUpdateStateCallback = { state, sensorType in
|
||||
if let sensor = sensorType, sensor == .AWAKE {
|
||||
self.locationPermissionOn = state == .on
|
||||
self.toggleViews()
|
||||
}
|
||||
}
|
||||
if !shouldShowPolicyUpdateMessage() {
|
||||
startAllSensors()
|
||||
}
|
||||
|
||||
updateAnimationViewWithAnimationName(name: "Spinner_home")
|
||||
|
||||
|
@ -253,6 +258,7 @@ class HomeViewController: UIViewController {
|
|||
|
||||
self?.toggleBluetoothStatusView()
|
||||
self?.toggleBluetoothPermissionStatusView()
|
||||
self?.toggleLocationPermissionStatusView()
|
||||
self?.toggleHeaderView()
|
||||
self?.toggleUploadView()
|
||||
self?.toggleUploadDateView()
|
||||
|
@ -306,8 +312,9 @@ class HomeViewController: UIViewController {
|
|||
fileprivate func readPermissions(notificationSettings: UNNotificationSettings) {
|
||||
self.bluetoothStatusOn = BluetraceManager.shared.isBluetoothOn()
|
||||
self.bluetoothPermissionOn = BluetraceManager.shared.isBluetoothAuthorized()
|
||||
self.locationPermissionOn = BluetraceManager.shared.isLocationOnAuthorized()
|
||||
self.pushNotificationOn = notificationSettings.authorizationStatus == .authorized
|
||||
let newAllPermissionsOn = self.bluetoothStatusOn && self.bluetoothPermissionOn
|
||||
let newAllPermissionsOn = self.bluetoothStatusOn && self.bluetoothPermissionOn && self.locationPermissionOn
|
||||
|
||||
if newAllPermissionsOn != self.allPermissionOn {
|
||||
self.allPermissionOn = newAllPermissionsOn
|
||||
|
@ -360,13 +367,17 @@ class HomeViewController: UIViewController {
|
|||
toggleViewVisibility(view: bluetoothPermissionOffView, isVisible: !self.allPermissionOn && !self.bluetoothPermissionOn && !registrationNeeded)
|
||||
}
|
||||
|
||||
fileprivate func toggleLocationPermissionStatusView() {
|
||||
toggleViewVisibility(view: locationPermissionsView, isVisible: !allPermissionOn && !locationPermissionOn && (bluetoothPermissionOn && bluetoothStatusOn) && !registrationNeeded)
|
||||
}
|
||||
|
||||
fileprivate func toggleRegistrationNeededView() {
|
||||
toggleViewVisibility(view: inactiveTokenExpiredView, isVisible: registrationNeeded)
|
||||
}
|
||||
|
||||
func attemptTurnOnBluetooth() {
|
||||
BluetraceManager.shared.toggleScanning(false)
|
||||
BluetraceManager.shared.turnOn()
|
||||
BluetraceManager.shared.turnOnBLE()
|
||||
}
|
||||
|
||||
fileprivate func updateAppActiveSubtitle() {
|
||||
|
@ -386,14 +397,13 @@ class HomeViewController: UIViewController {
|
|||
|
||||
func shouldShowPolicyUpdateMessage() -> Bool {
|
||||
// this is the min version that the disclamer should be diplayed on.
|
||||
let minVersionShowPolicyUpdate = 77
|
||||
let minVersionShowPolicyUpdate = 89
|
||||
|
||||
let latestVersionShown = UserDefaults.standard.integer(forKey: "latestPolicyUpdateVersionShown")
|
||||
guard let currentVersion = (Bundle.main.version as NSString?)?.integerValue else {
|
||||
return false
|
||||
}
|
||||
if currentVersion >= minVersionShowPolicyUpdate && currentVersion > latestVersionShown {
|
||||
UserDefaults.standard.set(currentVersion, forKey: "latestPolicyUpdateVersionShown")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
@ -401,14 +411,24 @@ class HomeViewController: UIViewController {
|
|||
|
||||
func showPolicyUpdateMessage() {
|
||||
|
||||
let privacyPolicyUrl = URLHelper.getPrivacyPolicyURL()
|
||||
let disclaimerAlert = CSAlertViewController(nibName: "CSAlertView", bundle: nil)
|
||||
let disclaimerMsg = NSMutableAttributedString(string: "collection_message".localizedString(), attributes: [.font : UIFont.preferredFont(forTextStyle: .body)])
|
||||
disclaimerMsg.addLink(enclosedIn: "*", urlString: privacyPolicyUrl)
|
||||
disclaimerAlert.set(message: disclaimerMsg, buttonLabel: "dismiss".localizedString())
|
||||
guard let currentVersion = (Bundle.main.version as NSString?)?.integerValue else {
|
||||
return
|
||||
}
|
||||
UserDefaults.standard.set(currentVersion, forKey: "latestPolicyUpdateVersionShown")
|
||||
let disclaimerAlert = CSGenericContentViewController(nibName: "CSGenericContentView", bundle: nil)
|
||||
let contentAttributedString = NSMutableAttributedString(string: "update_description".localizedString(), attributes: [
|
||||
.font: UIFont.preferredFont(forTextStyle: .body)
|
||||
])
|
||||
disclaimerAlert.contentViewModel = CSGenericContentViewModel(viewTitle: "update_heading".localizedString(),
|
||||
viewContentDescription: contentAttributedString,
|
||||
buttonLabel: "update_modal_button".localizedString(),
|
||||
buttonCallback: {
|
||||
self.startAllSensors()
|
||||
disclaimerAlert.dismiss(animated: true)
|
||||
})
|
||||
|
||||
disclaimerAlert.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
|
||||
disclaimerAlert.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
|
||||
disclaimerAlert.modalTransitionStyle = UIModalTransitionStyle.coverVertical
|
||||
present(disclaimerAlert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
@ -417,6 +437,12 @@ class HomeViewController: UIViewController {
|
|||
toggleViews()
|
||||
}
|
||||
|
||||
// MARK: Sensors
|
||||
|
||||
func startAllSensors() {
|
||||
BluetraceManager.shared.turnOnAllSensors()
|
||||
}
|
||||
|
||||
// MARK: API calls
|
||||
func getMessagesFromServer(force: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
let onMessagesDone: (MessageResponse?, CovidSafeAPIError?) -> Void = { (messageResponse, error) in
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// OnboardingStep1bViewController.swift
|
||||
// HowItWorksViewController.swift
|
||||
// CovidSafe
|
||||
//
|
||||
// Copyright © 2020 Australian Government. All rights reserved.
|
||||
|
@ -7,7 +7,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
class OnboardingStep1bViewController: UIViewController {
|
||||
class HowItWorksViewController: UIViewController {
|
||||
|
||||
@IBOutlet weak var textView: UITextView!
|
||||
|
|
@ -30,12 +30,19 @@
|
|||
<string>COVIDSafe exchanges Bluetooth® signals with nearby phones running the same app. These signals contain an anonymised ID, which is encrypted and changes continually to ensure your privacy.</string>
|
||||
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||
<string>COVIDSafe exchanges Bluetooth® signals with nearby phones running the same app. These signals contain an anonymised ID, which is encrypted and changes continually to ensure your privacy.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>COVIDSafe exchanges Bluetooth® signals with nearby phones running the same app. Location permission is required for best performance, your location is not recorded to ensure your privacy.</string>
|
||||
<key>NSLocationAlwaysUsageDescription</key>
|
||||
<string>COVIDSafe exchanges Bluetooth® signals with nearby phones running the same app. Location permission is required for best performance, your location is not recorded to ensure your privacy.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>COVIDSafe exchanges Bluetooth® signals with nearby phones running the same app. Location permission is required for best performance, your location is not recorded to ensure your privacy.</string>
|
||||
<key>TRACER_SVC_ID</key>
|
||||
<string>${SERVICE_UUID}</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>bluetooth-central</string>
|
||||
<string>bluetooth-peripheral</string>
|
||||
<string>location</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
|
|
|
@ -4,36 +4,55 @@ import UIKit
|
|||
|
||||
public extension NSMutableAttributedString {
|
||||
|
||||
enum ElementType {
|
||||
case Link,
|
||||
Bold
|
||||
}
|
||||
|
||||
func parseHTMLLinks() {
|
||||
while canParseHtmlOccurenceLink() {
|
||||
parseHtmlOccurenceLink()
|
||||
let regexLinkStartElementString = #"\<a(.*?)\>"#
|
||||
let regexLinkEndElementTextString = #"\<\/a\>"#
|
||||
while canParseOccurence(elementStartRegex: regexLinkStartElementString, elementEndRegex: regexLinkEndElementTextString) {
|
||||
parseHtmlOccurence(elementStartRegex: regexLinkStartElementString, elementEndRegex: regexLinkEndElementTextString, elementType: .Link)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func canParseHtmlOccurenceLink() -> Bool {
|
||||
let regexLinkStartElementString = #"\<a(.*?)\>"#
|
||||
let regexLinkEndElementTextString = #"\<\/a\>"#
|
||||
guard string.range(of: regexLinkStartElementString, options: .regularExpression) != nil else {
|
||||
func parseBoldTags() {
|
||||
let regexBoldStartElementString = #"\<b(.*?)\>"#
|
||||
let regexBoldEndElementTextString = #"\<\/b\>"#
|
||||
while canParseOccurence(elementStartRegex: regexBoldStartElementString, elementEndRegex: regexBoldEndElementTextString) {
|
||||
parseHtmlOccurence(elementStartRegex: regexBoldStartElementString, elementEndRegex: regexBoldEndElementTextString, elementType: .Bold)
|
||||
}
|
||||
}
|
||||
|
||||
func canParseOccurence(elementStartRegex: String, elementEndRegex: String) -> Bool {
|
||||
guard string.range(of: elementStartRegex, options: .regularExpression) != nil else {
|
||||
return false
|
||||
}
|
||||
guard string.range(of: regexLinkEndElementTextString, options: .regularExpression) != nil else {
|
||||
guard string.range(of: elementEndRegex, options: .regularExpression) != nil else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fileprivate func parseHtmlOccurenceLink() {
|
||||
let regexLinkStartElementString = #"\<a(.*?)\>"#
|
||||
let regexLinkEndElementTextString = #"\<\/a\>"#
|
||||
guard let strStartElementRange = string.range(of: regexLinkStartElementString, options: .regularExpression) else {
|
||||
fileprivate func parseHtmlOccurence(elementStartRegex: String, elementEndRegex: String, elementType: ElementType) {
|
||||
guard let strStartElementRange = string.range(of: elementStartRegex, options: .regularExpression) else {
|
||||
return
|
||||
}
|
||||
guard let strEndElementRange = string.range(of: regexLinkEndElementTextString, options: .regularExpression) else {
|
||||
guard let strEndElementRange = string.range(of: elementEndRegex, options: .regularExpression) else {
|
||||
return
|
||||
}
|
||||
|
||||
let convertedStartRange = NSRange(strStartElementRange, in: string)
|
||||
let convertedEndRange = NSRange(strEndElementRange, in: string)
|
||||
|
||||
let nsStartElementRange = NSRange(location: convertedStartRange.location, length: convertedStartRange.upperBound - convertedStartRange.lowerBound)
|
||||
let nsEndElementRange = NSRange(location: convertedEndRange.location, length: convertedEndRange.upperBound - convertedEndRange.lowerBound)
|
||||
|
||||
switch elementType {
|
||||
case .Link:
|
||||
//get the url string
|
||||
var urlString = ""
|
||||
let startElementStr = String(string[strStartElementRange])
|
||||
if let urlRange = startElementStr.range(of: #"\"(.*?)\""#, options: .regularExpression) {
|
||||
|
@ -44,17 +63,17 @@ public extension NSMutableAttributedString {
|
|||
let end = urlString.index(urlString.endIndex, offsetBy: -2)
|
||||
urlString = String(urlString[start...end])
|
||||
}
|
||||
|
||||
let convertedStartRange = NSRange(strStartElementRange, in: string)
|
||||
let convertedEndRange = NSRange(strEndElementRange, in: string)
|
||||
|
||||
let nsStartElementRange = NSRange(location: convertedStartRange.location, length: convertedStartRange.upperBound - convertedStartRange.lowerBound)
|
||||
let nsEndElementRange = NSRange(location: convertedEndRange.location, length: convertedEndRange.upperBound - convertedEndRange.lowerBound)
|
||||
|
||||
//remove html marking from text
|
||||
replaceCharacters(in: nsEndElementRange, with: "*")
|
||||
replaceCharacters(in: nsStartElementRange, with: "*")
|
||||
addLink(enclosedIn: "*", urlString: urlString)
|
||||
case .Bold:
|
||||
//remove bold marking from text
|
||||
replaceCharacters(in: nsEndElementRange, with: "#")
|
||||
replaceCharacters(in: nsStartElementRange, with: "#")
|
||||
addBold(enclosedIn: "#")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
@ -87,4 +106,34 @@ public extension NSMutableAttributedString {
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addBold(enclosedIn marker: String) -> Bool {
|
||||
guard !marker.isEmpty else { return false }
|
||||
|
||||
let regexString = "\(marker)(.*?)\(marker)"
|
||||
guard let strRange = string.range(of: regexString, options: .regularExpression) else {
|
||||
return false
|
||||
}
|
||||
let convertedRange = NSRange(strRange, in: string)
|
||||
|
||||
let matchingString = string[strRange]
|
||||
let enclosedString = matchingString.replacingOccurrences(of: marker, with: "")
|
||||
let nsBeginRange = NSRange(location: convertedRange.location, length: marker.count)
|
||||
let nsEndRange = NSRange(location: convertedRange.upperBound - marker.count, length: marker.count)
|
||||
// first replace end, otherwise the range will change
|
||||
replaceCharacters(in: nsEndRange, with: "")
|
||||
replaceCharacters(in: nsBeginRange, with: "")
|
||||
|
||||
let linkRange = NSRange(location: convertedRange.location, length: enclosedString.count)
|
||||
|
||||
// for now only supporting body. Need to get the UIFont from the current string.
|
||||
let attributes: [NSAttributedString.Key: Any] = [
|
||||
.font: UIFont.preferredFont(for: .body, weight: .semibold)
|
||||
]
|
||||
|
||||
addAttributes(attributes, range: linkRange)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -286,7 +286,7 @@ class OTPViewController: UIViewController, RegistrationHandler {
|
|||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
if let successVC = segue.destination as? OnboardingStep2bViewController {
|
||||
if let successVC = segue.destination as? RegistrationSuccessViewController {
|
||||
successVC.reauthenticating = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
//
|
||||
// OnboardingStep2ViewController.swift
|
||||
// CovidSafe
|
||||
//
|
||||
// Copyright © 2020 Australian Government. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreBluetooth
|
||||
import UserNotifications
|
||||
|
||||
class OnboardingStep2ViewController: UIViewController {
|
||||
private var bluetoothDidUpdateStateCallback: ((CBManagerState) -> Void)?
|
||||
|
||||
@IBOutlet weak var stepCounterLabel: UILabel!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
stepCounterLabel.text = String.localizedStringWithFormat( "stepCounter".localizedString(),
|
||||
4,
|
||||
4
|
||||
)
|
||||
}
|
||||
|
||||
@IBAction func proceedTapped(_ sender: UIButton) {
|
||||
self.bluetoothDidUpdateStateCallback = BluetraceManager.shared.bluetoothDidUpdateStateCallback
|
||||
BluetraceManager.shared.bluetoothDidUpdateStateCallback = centralDidUpdateStateCallback
|
||||
BluetraceManager.shared.turnOn()
|
||||
UserDefaults.standard.set(true, forKey: "turnedOnBluetooth")
|
||||
}
|
||||
|
||||
func centralDidUpdateStateCallback(_ state: CBManagerState) {
|
||||
DLog("state changed in permission request to \(BluetraceUtils.centralStateToString(state))")
|
||||
requestPushPermissions()
|
||||
}
|
||||
|
||||
func requestPushPermissions() {
|
||||
BluetraceManager.shared.bluetoothDidUpdateStateCallback = self.bluetoothDidUpdateStateCallback
|
||||
UNUserNotificationCenter.current()
|
||||
.requestAuthorization(options: [.alert, .sound, .badge]) {
|
||||
granted, error in
|
||||
|
||||
UserDefaults.standard.set(true, forKey: "allowedPermissions")
|
||||
print("Permissions granted: \(granted)")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
self.performSegue(withIdentifier: "showSuccessSegue", sender: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
import CoreBluetooth
|
||||
|
||||
public struct PeripheralCharacteristicsData: Codable {
|
||||
var modelP: String // phone model of peripheral
|
||||
var msg: String // tempID
|
||||
var org: String
|
||||
var v: Int
|
||||
}
|
||||
|
||||
let TRACER_SVC_ID: CBUUID = CBUUID(string: "\(PlistHelper.getvalueFromInfoPlist(withKey: "TRACER_SVC_ID") ?? "B82AB3FC-1595-4F6A-80F0-FE094CC218F9")")
|
||||
let TRACER_SVC_CHARACTERISTIC_ID = CBUUID(string: "\(PlistHelper.getvalueFromInfoPlist(withKey: "TRACER_SVC_ID") ?? "B82AB3FC-1595-4F6A-80F0-FE094CC218F9")")
|
||||
let ORG_ID = "<your org id here>"
|
||||
let PROTOCOL_VERSION = 1
|
||||
|
||||
public class PeripheralController: NSObject {
|
||||
|
||||
enum PeripheralError: Error {
|
||||
case peripheralAlreadyOn
|
||||
case peripheralAlreadyOff
|
||||
}
|
||||
|
||||
struct CachedPayload {
|
||||
var payload: Data,
|
||||
expiry: TimeInterval
|
||||
}
|
||||
|
||||
var didUpdateState: ((String) -> Void)?
|
||||
private let encounteredCentralExpiryTime:TimeInterval = 1800.0 // 30 minutes
|
||||
private let restoreIdentifierKey = "com.joelkek.tracer.peripheral"
|
||||
private let peripheralName: String
|
||||
private var encounteredCentrals = [UUID: (EncounterRecord)]()
|
||||
private var payloadLookaside = [UUID: CachedPayload]()
|
||||
private let FREQUENCY_OF_CONNECTION_IN_S = 20.0
|
||||
private var characteristicData: PeripheralCharacteristicsData
|
||||
|
||||
private var peripheral: CBPeripheralManager!
|
||||
private var queue: DispatchQueue
|
||||
private lazy var readableCharacteristic = CBMutableCharacteristic(type: BluetraceConfig.BluetoothServiceID, properties: [.read, .write, .writeWithoutResponse], value: nil, permissions: [.readable, .writeable])
|
||||
|
||||
public init(peripheralName: String, queue: DispatchQueue) {
|
||||
DLog("PC init")
|
||||
self.queue = queue
|
||||
self.peripheralName = peripheralName
|
||||
self.characteristicData = PeripheralCharacteristicsData(modelP: DeviceIdentifier.getModel(), msg: "<unknown>", org: BluetraceConfig.OrgID, v: BluetraceConfig.ProtocolVersion)
|
||||
super.init()
|
||||
}
|
||||
|
||||
public func turnOn() {
|
||||
guard peripheral == nil else {
|
||||
return
|
||||
}
|
||||
peripheral = CBPeripheralManager(delegate: self, queue: self.queue, options: [CBPeripheralManagerOptionRestoreIdentifierKey: restoreIdentifierKey, CBPeripheralManagerOptionShowPowerAlertKey: 1])
|
||||
}
|
||||
|
||||
public func turnOff() {
|
||||
guard peripheral != nil else {
|
||||
return
|
||||
}
|
||||
peripheral.stopAdvertising()
|
||||
peripheral = nil
|
||||
}
|
||||
|
||||
public func getState() -> String {
|
||||
return BluetraceUtils.centralStateToString(peripheral.state)
|
||||
}
|
||||
}
|
||||
|
||||
extension PeripheralController: CBPeripheralManagerDelegate {
|
||||
|
||||
public func peripheralManager(_ peripheral: CBPeripheralManager,
|
||||
willRestoreState dict: [String : Any]) {
|
||||
DLog("PC willRestoreState")
|
||||
}
|
||||
|
||||
public func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
|
||||
DLog("PC peripheralManagerDidUpdateState. Current state: \(BluetraceUtils.centralStateToString(peripheral.state))")
|
||||
didUpdateState?(BluetraceUtils.centralStateToString(peripheral.state))
|
||||
guard peripheral.state == .poweredOn else { return }
|
||||
let advertisementData: [String: Any] = [CBAdvertisementDataLocalNameKey: peripheralName,
|
||||
CBAdvertisementDataServiceUUIDsKey: [BluetraceConfig.BluetoothServiceID]]
|
||||
let tracerService = CBMutableService(type: BluetraceConfig.BluetoothServiceID, primary: true)
|
||||
tracerService.characteristics = [readableCharacteristic]
|
||||
peripheral.removeAllServices()
|
||||
peripheral.add(tracerService)
|
||||
peripheral.startAdvertising(advertisementData)
|
||||
}
|
||||
|
||||
public func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
|
||||
DLog("\(["request": request] as AnyObject)")
|
||||
let remoteCentral = request.central.identifier;
|
||||
|
||||
// clean up expired payloads
|
||||
cleanUpExpiredCachedPayloads()
|
||||
|
||||
if request.offset == 0 {
|
||||
// new request, create new payload
|
||||
EncounterMessageManager.shared.getAdvertisementPayload { (payloadToAdvertise) in
|
||||
self.queue.async {
|
||||
if let payload = payloadToAdvertise {
|
||||
// cache payload for remote
|
||||
self.payloadLookaside[remoteCentral] = CachedPayload(payload: payload, expiry: Date().timeIntervalSince1970 + self.FREQUENCY_OF_CONNECTION_IN_S);
|
||||
|
||||
|
||||
request.value = payload.advanced(by: request.offset)
|
||||
peripheral.respond(to: request, withResult: .success)
|
||||
} else {
|
||||
DLog("Error getting payload to advertise")
|
||||
peripheral.respond(to: request, withResult: .unlikelyError)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// get cached payload, check offset valid
|
||||
guard let cachedPayload = self.payloadLookaside[remoteCentral] else {
|
||||
peripheral.respond(to: request, withResult: .unlikelyError)
|
||||
return
|
||||
}
|
||||
|
||||
if request.offset > cachedPayload.payload.count {
|
||||
peripheral.respond(to: request, withResult: .invalidOffset)
|
||||
return
|
||||
}
|
||||
|
||||
if request.offset == cachedPayload.payload.count {
|
||||
// the central already read all the data in its last read request
|
||||
peripheral.respond(to: request, withResult: .success)
|
||||
return
|
||||
}
|
||||
|
||||
// return payload as normal
|
||||
request.value = cachedPayload.payload.advanced(by: request.offset)
|
||||
peripheral.respond(to: request, withResult: .success)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func cleanUpExpiredCachedPayloads() {
|
||||
for payloadKey in payloadLookaside.keys {
|
||||
let currentTime = Date().timeIntervalSince1970
|
||||
guard let payload = payloadLookaside[payloadKey], payload.expiry < currentTime else {
|
||||
continue
|
||||
}
|
||||
// if payload exists and expiry time is less than current time, remove.
|
||||
payloadLookaside.removeValue(forKey: payloadKey)
|
||||
}
|
||||
}
|
||||
|
||||
public func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
|
||||
let debugLogs = ["requests": requests as AnyObject,
|
||||
"reqValue": String(data: requests[0].value!, encoding: .utf8) ?? "<nil>"] as AnyObject
|
||||
DLog("\(debugLogs)")
|
||||
do {
|
||||
for request in requests {
|
||||
if let characteristicValue = request.value {
|
||||
let dataFromCentral = try JSONDecoder().decode(CentralWriteData.self, from: characteristicValue)
|
||||
let encounter = EncounterRecord(from: dataFromCentral)
|
||||
if (shouldRecordEncounterWithCentral(request.central)) {
|
||||
try encounter.saveRemoteCentralToCoreData()
|
||||
encounteredCentrals.updateValue(encounter, forKey: request.central.identifier)
|
||||
removeOldEncounters()
|
||||
} else {
|
||||
DLog("Encounterd central too recently, not recording")
|
||||
}
|
||||
}
|
||||
}
|
||||
peripheral.respond(to: requests[0], withResult: .success)
|
||||
} catch {
|
||||
DLog("Error: \(error)")
|
||||
peripheral.respond(to: requests[0], withResult: .unlikelyError)
|
||||
}
|
||||
}
|
||||
|
||||
private func removeOldEncounters() {
|
||||
encounteredCentrals = encounteredCentrals.filter { (uuid, encounter) -> Bool in
|
||||
guard let encDate = encounter.timestamp else {
|
||||
return true
|
||||
}
|
||||
return abs(encDate.timeIntervalSinceNow) < encounteredCentralExpiryTime
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldRecordEncounterWithCentral(_ central: CBCentral) -> Bool {
|
||||
guard let previousEncounter = encounteredCentrals[central.identifier] else {
|
||||
return true
|
||||
}
|
||||
guard let lastEncDate = previousEncounter.timestamp else {
|
||||
return true
|
||||
}
|
||||
|
||||
if abs(lastEncDate.timeIntervalSinceNow) > BluetraceConfig.CentralScanInterval {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// OnboardingStep1aViewController.swift
|
||||
// PrivacyPolicyViewController.swift
|
||||
// CovidSafe
|
||||
//
|
||||
// Copyright © 2020 Australian Government. All rights reserved.
|
||||
|
@ -9,7 +9,7 @@ import UIKit
|
|||
import KeychainSwift
|
||||
import SafariServices
|
||||
|
||||
class OnboardingStep1aViewController: UIViewController, UITextViewDelegate {
|
||||
class PrivacyPolicyViewController: UIViewController, UITextViewDelegate {
|
||||
|
||||
|
||||
@IBOutlet weak var openLink: UILabel!
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// OnboardingStep1ViewController.swift
|
||||
// RegistrationIntroViewController.swift
|
||||
// CovidSafe
|
||||
//
|
||||
// Copyright © 2020 Australian Government. All rights reserved.
|
||||
|
@ -9,7 +9,7 @@ import UIKit
|
|||
import SafariServices
|
||||
import KeychainSwift
|
||||
|
||||
class OnboardingStep1ViewController: UIViewController {
|
||||
class RegistrationIntroViewController: UIViewController {
|
||||
|
||||
let keychain = KeychainSwift()
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// OnboardingStep2bViewController.swift
|
||||
// RegistrationSuccessViewController.swift
|
||||
// CovidSafe
|
||||
//
|
||||
// Copyright © 2020 Australian Government. All rights reserved.
|
||||
|
@ -8,7 +8,7 @@
|
|||
import UIKit
|
||||
import SafariServices
|
||||
|
||||
class OnboardingStep2bViewController: UIViewController {
|
||||
class RegistrationSuccessViewController: UIViewController {
|
||||
@IBOutlet weak var pointOneLabel: UILabel!
|
||||
@IBOutlet weak var pointTwoLabel: UILabel!
|
||||
@IBOutlet weak var pointThreeLabel: UILabel!
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
|
@ -206,9 +206,6 @@
|
|||
<constraint firstAttribute="bottom" secondItem="nm4-W8-yfz" secondAttribute="bottom" constant="1" id="Rog-yq-MUA"/>
|
||||
<constraint firstItem="nm4-W8-yfz" firstAttribute="top" secondItem="1F3-ji-p1Z" secondAttribute="top" id="SY0-Kp-41h"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="localVOLabelKey" value="AllowBluetoothOFF_VOLabel"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="9uF-DB-1IF" appends="YES" id="MIg-P4-8ip"/>
|
||||
</connections>
|
||||
|
@ -266,9 +263,6 @@
|
|||
<constraint firstAttribute="trailing" secondItem="2Uq-lN-t0Y" secondAttribute="trailing" id="dJP-zw-VDV"/>
|
||||
<constraint firstItem="2Uq-lN-t0Y" firstAttribute="leading" secondItem="syj-9c-EKO" secondAttribute="leading" id="ipO-6X-Lp6"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="localVOLabelKey" value="BluetoothOFF_VOLabel"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="YgE-FC-hoe" appends="YES" id="ODK-ba-1bP"/>
|
||||
</connections>
|
||||
|
|
|
@ -12,11 +12,22 @@ extension UITextView {
|
|||
tintColor = UIColor.covidSafeColor
|
||||
}
|
||||
|
||||
func parseHTMLLinks() {
|
||||
func addAllBold(enclosedIn marker: String) {
|
||||
guard let attributedText = attributedText else { return }
|
||||
|
||||
let mutableString = NSMutableAttributedString(attributedString: attributedText)
|
||||
while mutableString.canParseOccurence(elementStartRegex: marker, elementEndRegex: marker) {
|
||||
mutableString.addBold(enclosedIn: marker)
|
||||
}
|
||||
self.attributedText = mutableString
|
||||
}
|
||||
|
||||
func parseHTMLTags() {
|
||||
guard let attributedText = attributedText else { return }
|
||||
|
||||
let mutableString = NSMutableAttributedString(attributedString: attributedText)
|
||||
mutableString.parseHTMLLinks()
|
||||
mutableString.parseBoldTags()
|
||||
self.attributedText = mutableString
|
||||
tintColor = UIColor.covidSafeColor
|
||||
}
|
||||
|
|
|
@ -5,4 +5,7 @@
|
|||
"BluetoothUsageDesc" = "يتبادل COVIDSafe إشارات Bluetooth® مع الهواتف المجاورة التي تشغِّل نفس التطبيق. تحتوي هذه الإشارات على معرِّف مجهول الهوية، يتم تشفيره وتغييره باستمرار لضمان الحفاظ على خصوصيتك.";
|
||||
"NSBluetoothAlwaysUsageDescription" = "يتبادل COVIDSafe إشارات Bluetooth® مع الهواتف المجاورة التي تشغِّل نفس التطبيق. تحتوي هذه الإشارات على معرِّف مجهول الهوية، يتم تشفيره وتغييره باستمرار لضمان الحفاظ على خصوصيتك.";
|
||||
"NSBluetoothPeripheralUsageDescription" = "يتبادل COVIDSafe إشارات Bluetooth® مع الهواتف المجاورة التي تشغِّل نفس التطبيق. تحتوي هذه الإشارات على معرِّف مجهول الهوية، يتم تشفيره وتغييره باستمرار لضمان الحفاظ على خصوصيتك.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "";
|
||||
"NSLocationAlwaysUsageDescription" = "";
|
||||
"NSLocationWhenInUseUsageDescription" = "";
|
||||
"UILaunchStoryboardName" = "LaunchScreen_ar";
|
||||
|
|
|
@ -239,7 +239,7 @@
|
|||
/* OnBoarding Data Privacy */
|
||||
"data_privacy_headline" = "التسجيل والخصوصية";
|
||||
"deaths" = "الوفيات";
|
||||
"dialog_error_uploading_message" = "حدث خطأ أثناء تحميل معلوماتك، يرجى المحاولة مرة أخرى.";
|
||||
"dialog_error_uploading_message" = "";
|
||||
"dialog_error_uploading_negative" = "إلغاء";
|
||||
"dialog_error_uploading_positive" = "حاول مرة أخرى";
|
||||
"dialog_uploading_message" = "يتم الآن تحميل معلومات COVIDSafe الخاصة بك. \n\n الرجاء عدم إغلاق التطبيق.";
|
||||
|
@ -317,6 +317,8 @@
|
|||
"jwt_heading" = "يرجى التسجيل مرة أخرى";
|
||||
"jwt_success" = "";
|
||||
"loading_numbers" = "تحميل أحدث الأرقام";
|
||||
"location_off" = "";
|
||||
"location_off_description" = "";
|
||||
/* Splash Screen */
|
||||
"migration_in_progress" = "تحديث تطبيق COVIDSafe قيد الإنجاز. \n\nيُرجى التأكد من عدم إغلاق هاتفك إلى أن يتم اكتمال التحديث.";
|
||||
"national_numbers" = "الأرقام على الصعيد الوطني";
|
||||
|
@ -343,9 +345,8 @@
|
|||
"options_for_australia" = "خيارات لأستراليا";
|
||||
"permission_button" = "تابع";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS" = "يحتاج تطبيق COVIDSafe إلى تمكين Bluetooth® لكي يعمل. يسمح تمكين الإشعارات بتلقي تحديثات لتذكيرك عندما يكون تطبيق COVIDSafe متوقف عن العمل. \n\n اختر \"تابع\" للتمكين:";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS_2" = "1. Bluetooth® \n 2. الإشعارات \n\n لا يرسل تطبيق COVIDSafe طلبات الاقتران.";
|
||||
"permission_content_iOS" = "";
|
||||
"permission_content_iOS_2" = "";
|
||||
/* OnBoarding Permission */
|
||||
"permission_headline" = "إعدادات التطبيق";
|
||||
"permission_success_button" = "تمّ";
|
||||
|
@ -412,10 +413,15 @@
|
|||
"update_available_dismiss_btn" = "ذكّرني لاحقًا";
|
||||
"update_available_message_ios" = "لقد أجرينا تحسينات على تطبيق COVIDSafe. قم بتحديث التطبيق عبر متجر التطبيقات App Store.";
|
||||
"update_available_title" = "التحديث متوفر!";
|
||||
"update_description" = "";
|
||||
"update_description_VO" = "";
|
||||
"update_heading" = "";
|
||||
"update_modal_button" = "تابع";
|
||||
"upload_answer_no" = "لا";
|
||||
/* Upload flow */
|
||||
"upload_answer_yes" = "نعم";
|
||||
"upload_consent_button" = "أوافق";
|
||||
"upload_fail_heading" = "";
|
||||
"upload_failed" = "فشل التحميل";
|
||||
"upload_finished_header" = "شكراً لك على المساعدة في وقف انتشار COVID-19!";
|
||||
"upload_finished_sub_header" = "لقد قمت بتحميل معلوماتك بنجاح إلى نظام تخزين COVIDSafe العالي الأمان. \n\n سيقوم موظفو دائرة الصحة في الولاية أو الإقليم بإخطار مستخدمي تطبيق COVIDSafe الآخرين الذين سجلوا حالات اختلاط عن قرب معك. ستظل هويتك مجهولة للمستخدمين الآخرين.";
|
||||
|
@ -501,7 +507,7 @@
|
|||
"NotificationsDisabled_VOLabel" = "الإشعارات معطّلة. لن تتلقى إشعارًا إذا كان تطبيق COVIDSafe لا يعمل.\nقم بتغيير إعدادات الإشعارات";
|
||||
"NotificationsEnabled_VOLabel" = "يُسمح بالإشعارات. سوف تتلقى إشعارًا إذا كان تطبيق COVIDSafe لا يعمل.\nقم بتغيير إعدادات الإشعارات";
|
||||
"OS1b_TopParagraph_VOLabel" = "تُستخدم إشارات Bluetooth لتحديد وجودك بالقرب من مستخدم آخر لتطبيق COVIDSafe. \n سيتم تدوين ملاحظة في كل مرة يحصل فيها اختلاط عن قرب بينك وبين مستخدمي تطبيق COVIDSafe الآخرين لجمع معلومات عن اختلاط الأشخاص. يتم تشفير هذه المعلومات وتخزينها فقط في هاتفك. \n إذا جاءت نتيجة فحصك ﻟ COVID-19 إيجابية كمستخدم لتطبيق COVIDSafe، سوف يتصل بك أحد موظفي دائرة الصحة في الولاية أو الإقليم ويساعدك في القيام بتحميل اختياري لمعلومات الاتصال بك إلى نظام تخزين معلومات آمن للغاية \nيمكن أيضًا لموظفي دائرة الصحة في الولاية أو الإقليم الاتصال بك إذا حصل اتصال وثيق بينك وبين مستخدم آخر لتطبيق COVIDSafe أظهرت نتائج الفحص أنه مصاب بالفيروس. \n لمزيد من المعلومات، يرجى الرجوع إلى صفحة مواضيع المساعدة.";
|
||||
"permission_content_iOS_2_VOLabel" = "1. Bluetooth \n 2. الإشعارات \n\n لا يرسل تطبيق COVIDSafe طلبات الاقتران.";
|
||||
"permission_content_iOS_VOLabel" = "يحتاج تطبيق COVIDSafe إلى تمكين Bluetooth لكي يعمل. يسمح تمكين الإشعارات بتلقي تحديثات لتذكيرك عندما يكون تطبيق COVIDSafe متوقف عن العمل. \n\n اختر \"تابع\" للتمكين:";
|
||||
"permission_content_iOS_2_VOLabel" = "";
|
||||
"permission_content_iOS_VOLabel" = "";
|
||||
"ShareCovidSafe_VOLabel" = "شارك COVIDSafe. قم بدعوة الآخرين للمساعدة. معاً، نحن أقوى.";
|
||||
"UploadData_VOLabel" = "هل اتصل بك أحد موظفي دائرة الصحة؟ يمكنك تحميل معلوماتك فقط إذا كانت نتيجة فحصك إيجابية.";
|
||||
|
|
|
@ -5,4 +5,7 @@
|
|||
"BluetoothUsageDesc" = "Η COVIDSafe ανταλλάσσει σήματα Bluetooth® με κοντινά τηλέφωνα που χρησιμοποιούν την ίδια εφαρμογή. Αυτά τα σήματα περιέχουν ένα ανώνυμο αναγνωριστικό, το οποίο είναι κρυπτογραφημένο και αλλάζει συνεχώς για διασφάλιση του απορρήτου σας.";
|
||||
"NSBluetoothAlwaysUsageDescription" = "Η COVIDSafe ανταλλάσσει σήματα Bluetooth® με τηλέφωνα που βρίσκονται κοντά σας και χρησιμοποιούν την ίδια εφαρμογή. Αυτά τα σήματα έχουν ανώνυμο αναγνωριστικό, το οποίο είναι κρυπτογραφημένο και αλλάζει συνεχώς για διασφάλιση του απορρήτου σας.";
|
||||
"NSBluetoothPeripheralUsageDescription" = "Η COVIDSafe ανταλλάσσει σήματα Bluetooth® με τηλέφωνα που βρίσκονται κοντά σας και χρησιμοποιούν την ίδια εφαρμογή. Αυτά τα σήματα έχουν ανώνυμο αναγνωριστικό, το οποίο είναι κρυπτογραφημένο και αλλάζει συνεχώς για διασφάλιση του απορρήτου σας.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "";
|
||||
"NSLocationAlwaysUsageDescription" = "";
|
||||
"NSLocationWhenInUseUsageDescription" = "";
|
||||
"UILaunchStoryboardName" = "LaunchScreen_el";
|
||||
|
|
|
@ -239,7 +239,7 @@
|
|||
/* OnBoarding Data Privacy */
|
||||
"data_privacy_headline" = "Εγγραφή και προστασία του απορρήτου";
|
||||
"deaths" = "Θάνατοι";
|
||||
"dialog_error_uploading_message" = "Παρουσιάστηκε πρόβλημα κατά την ανάρτηση των στοιχείων σας. Δοκιμάστε ξανά.";
|
||||
"dialog_error_uploading_message" = "";
|
||||
"dialog_error_uploading_negative" = "Ακύρωση";
|
||||
"dialog_error_uploading_positive" = "Προσπαθήστε ξανά";
|
||||
"dialog_uploading_message" = "Τα στοιχεία σας αναρτίζονται στην COVIDSafe αυτή τη στιγμή. \n\nΜην κλείσετε την εφαρμογή.";
|
||||
|
@ -317,6 +317,8 @@
|
|||
"jwt_heading" = "Παρακαλούμε εγγραφείτε ξανά";
|
||||
"jwt_success" = "";
|
||||
"loading_numbers" = "Φόρτωση τελευταίων αριθμών";
|
||||
"location_off" = "";
|
||||
"location_off_description" = "";
|
||||
/* Splash Screen */
|
||||
"migration_in_progress" = "Η ενημέρωση της COVIDSafe συνεχίζεται. \n\nΒεβαιωθείτε ότι το τηλέφωνό σας δεν είναι απενεργοποιημένο έως ότου ολοκληρωθεί η ενημέρωση.";
|
||||
"national_numbers" = "Στατιστικές για όλη την Αυστραλία";
|
||||
|
@ -343,9 +345,8 @@
|
|||
"options_for_australia" = "Επιλογές για την Αυστραλία";
|
||||
"permission_button" = "Προχωρήστε";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS" = "Για να λειτουργήσει η COVIDSafe θα πρέπει το Bluetooth® να είναι ενεργοποιημένο. Όταν ενεργοποιήσετε τις Ειδοποιήσεις, θα λάβετε τις ενημερώσεις που υπενθυμίζουν πότε η COVIDSafe δεν είναι ενεργή. \n\n Επιλέξτε «Προχωρήστε» για ενεργοποίηση:";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS_2" = "1. Bluetooth® \n2. Ειδοποιήσεις \n\n Η COVIDSafe δεν αποστέλλει αιτήματα σύζευξης.";
|
||||
"permission_content_iOS" = "";
|
||||
"permission_content_iOS_2" = "";
|
||||
/* OnBoarding Permission */
|
||||
"permission_headline" = "Ρυθμίσεις εφαρμογής";
|
||||
"permission_success_button" = "Ολοκληρώθηκε";
|
||||
|
@ -412,10 +413,15 @@
|
|||
"update_available_dismiss_btn" = "Θύμισέ μου αργότερα";
|
||||
"update_available_message_ios" = "Πραγματοποιήσαμε βελτιώσεις στην COVIDSafe. Ενημερώστε την εφαρμογή μέσῳ του App Store.";
|
||||
"update_available_title" = "Η ενημερωμένη έκδοση είναι διαθέσιμη!";
|
||||
"update_description" = "";
|
||||
"update_description_VO" = "";
|
||||
"update_heading" = "";
|
||||
"update_modal_button" = "Προχωρήστε";
|
||||
"upload_answer_no" = "Όχι";
|
||||
/* Upload flow */
|
||||
"upload_answer_yes" = "Ναι";
|
||||
"upload_consent_button" = "Συμφωνώ";
|
||||
"upload_fail_heading" = "";
|
||||
"upload_failed" = "Η ανάρτηση απέτυχε";
|
||||
"upload_finished_header" = "Σας ευχαριστούμε που βοηθήσατε να σταματήσει η εξάπλωση του COVID-19!";
|
||||
"upload_finished_sub_header" = "Έχετε ανεβάσει με επιτυχία τις πληροφορίες σας στο σύστημα αποθήκευσης υψηλής ασφάλειας COVIDSafe.\n\nΟι υγειονομικοί υπάλληλοι της Πολιτείας ή της Επικράτειάς σας θα ενημερώσουν άλλους χρήστες της COVIDSafe που έχουν καταγράψει στενή επαφή μαζί σας. Η ταυτότητά σας θα παραμείνει ανώνυμη σε άλλους χρήστες.";
|
||||
|
@ -501,7 +507,7 @@
|
|||
"NotificationsDisabled_VOLabel" = "Οι ειδοποιήσεις είναι απενεργοποιημένες. Δε θα λάβετε ειδοποίηση εάν η COVIDSafe δεν είναι ενεργή. \nΑλλάξετε τις ρυθμίσεις ειδοποίησης";
|
||||
"NotificationsEnabled_VOLabel" = "Οι ειδοποιήσεις είναι ενεργοποιημένες. Θα λάβετε ειδοποίηση εάν η COVIDSafe δεν είναι ενεργή. \nΑλλαγή ρυθμίσεων ειδοποίησης";
|
||||
"OS1b_TopParagraph_VOLabel" = "Τα σήματα Bluetooth χρησιμοποιούνται για να προσδιορίσουν πότε βρίσκεστε κοντά σε άλλο χρήστη της COVIDSafe. \nΚάθε στενή επαφή σας με άλλους χρήστες της COVIDSafe σημειώνεται για να δημιουργεί στενά στοιχεία επικοινωνίας. Οι πληροφορίες είναι κρυπτογραφημένες και αποθηκεύονται μόνο στο τηλέφωνό σας. \nΕάν έχετε διαγνωστεί θετικά στον COVID-19 κατόπιν χρήσης της COVIDSafe, ένας υπεύθυνος υγείας της Πολιτείας ή της Επικράτειάς σας θα επικοινωνήσει μαζί σας. Θα βοηθήσει με την εθελοντική ανάρτηση των προσωπικών στοιχείων επικοινωνίας σας σε ένα εξαιρετικά ασφαλές σύστημα αποθήκευσης πληροφοριών. \nΟι υπεύθυνοι υγείας της Πολιτείας ή της Επικράτειάς σας μπορούν επίσης να επικοινωνήσουν μαζί σας εάν ήρθατε σε στενή επαφή με άλλο χρήστη της COVIDSafe που επίσης διαγνώστηκε θετικά. \nΓια περισσότερες πληροφορίες, πηγαίνετε στη σελίδα Θέματα Βοήθειας.";
|
||||
"permission_content_iOS_2_VOLabel" = "1. Bluetooth \n2. Ειδοποιήσεις \n\n Η COVIDSafe δεν αποστέλλει αιτήματα σύζευξης.";
|
||||
"permission_content_iOS_VOLabel" = "Η COVIDSafe για να λειτουργήσει θα πρέπει το Bluetooth να είναι ενεργοποιημένο. Όταν ενεργοποιήσετε τις Ειδοποιήσεις, θα λάβετε τις ενημερώσεις που υπενθυμίζουν πότε η COVIDSafe δεν είναι ενεργή. \n\n Επιλέξτε «Συνέχεια» για ενεργοποίηση:";
|
||||
"permission_content_iOS_2_VOLabel" = "";
|
||||
"permission_content_iOS_VOLabel" = "";
|
||||
"ShareCovidSafe_VOLabel" = "Μοιραστείτε την COVIDSafe. Καλέστε και άλλους να βοηθήσουν. Μαζί, είμαστε ισχυρότεροι.";
|
||||
"UploadData_VOLabel" = "Έχει επικοινωνήσει μαζί σας υπάλληλος υγείας; Μπορείτε να αναρτήσετε τις πληροφορίες σας μόνο αν έχετε διαγνωστεί θετικά.";
|
||||
|
|
|
@ -5,4 +5,7 @@
|
|||
"BluetoothUsageDesc" = "COVIDSafe exchanges Bluetooth® signals with nearby phones running the same app. These signals contain an anonymised ID, which is encrypted and changes continually to ensure your privacy.";
|
||||
"NSBluetoothAlwaysUsageDescription" = "COVIDSafe exchanges Bluetooth® signals with nearby phones running the same app. These signals contain an anonymised ID, which is encrypted and changes continually to ensure your privacy.";
|
||||
"NSBluetoothPeripheralUsageDescription" = "COVIDSafe exchanges Bluetooth® signals with nearby phones running the same app. These signals contain an anonymised ID, which is encrypted and changes continually to ensure your privacy.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Select ‘Always Allow’.\n\nCOVIDSafe does NOT track or store your location data.";
|
||||
"NSLocationAlwaysUsageDescription" = "Select ‘Always Allow’.\n\nCOVIDSafe does NOT track or store your location data.";
|
||||
"NSLocationWhenInUseUsageDescription" = "Select ‘While Using App’.\n\nCOVIDSafe does NOT track or store your location data.";
|
||||
"UILaunchStoryboardName" = "LaunchScreen_en";
|
||||
|
|
|
@ -239,7 +239,7 @@
|
|||
/* OnBoarding Data Privacy */
|
||||
"data_privacy_headline" = "Registration and privacy";
|
||||
"deaths" = "Deaths";
|
||||
"dialog_error_uploading_message" = "An error occurred while uploading your information, please try again.";
|
||||
"dialog_error_uploading_message" = "Try uploading your data again.\n\nRefer to this error code if a state or territory health official asks for it: %@";
|
||||
"dialog_error_uploading_negative" = "Cancel";
|
||||
"dialog_error_uploading_positive" = "Try again";
|
||||
"dialog_uploading_message" = "Your COVIDSafe information is currently being uploaded.\n\nPlease do not close the app.";
|
||||
|
@ -317,6 +317,8 @@
|
|||
"jwt_heading" = "Please register again";
|
||||
"jwt_success" = "Registration successfully renewed";
|
||||
"loading_numbers" = "Loading latest numbers";
|
||||
"location_off" = "Location: OFF";
|
||||
"location_off_description" = "Your iPhone requires Location permission for COVIDSafe to work. COVIDSafe does NOT track or store your location data.";
|
||||
/* Splash Screen */
|
||||
"migration_in_progress" = " COVIDSafe update in progress. \n\n Please make sure you phone is not switched off until the update is complete.";
|
||||
"national_numbers" = "National numbers";
|
||||
|
@ -343,9 +345,8 @@
|
|||
"options_for_australia" = "Options for Australia";
|
||||
"permission_button" = "Proceed";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS" = "COVIDSafe needs Bluetooth® enabled to work. By enabling Notifications, you get updates to remind you when COVIDSafe is not active. \n\nSelect ‘Proceed' to enable:";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS_2" = "1. Bluetooth®\n2. Notifications\n\nCOVIDSafe does not send pairing requests.";
|
||||
"permission_content_iOS" = "COVIDSafe needs #Bluetooth# enabled to work. COVIDSafe does not send pairing requests.\n\nBy enabling #Notifications#, you get updates to remind you when COVIDSafe is not active. \n\nYour iPhone requires #Location permission# for COVIDSafe to work. *COVIDSafe does NOT track or store your location data*\n\nSelect ‘Proceed' to enable:";
|
||||
"permission_content_iOS_2" = "1. Bluetooth\n2. Notifications\n3. Location Services";
|
||||
/* OnBoarding Permission */
|
||||
"permission_headline" = "App settings";
|
||||
"permission_success_button" = "Done";
|
||||
|
@ -412,10 +413,15 @@
|
|||
"update_available_dismiss_btn" = "Remind me later";
|
||||
"update_available_message_ios" = "We’ve been making improvements to COVIDSafe. Update through the App Store";
|
||||
"update_available_title" = "Update available!";
|
||||
"update_description" = "This update improves COVIDSafe’s Bluetooth performance. Now it better captures your close contacts.\n\nThis update requests access to your Location Services for COVIDSafe to work. This permission wakes COVIDSafe up every time you unlock your screen. <a href=\"https://www.covidsafe.gov.au/help-topics.html#location-permissions\">COVIDSafe does NOT collect, store or use your location data</a>\n\nThis is one of the new ways that COVIDSafe can better capture your close contacts. <a href=\"https://www.covidsafe.gov.au/help-topics.html#herald\">Find out more about how we’ve improved COVIDSafe</a>";
|
||||
"update_description_VO" = "This update improves COVIDSafe’s Bluetooth performance. Now it better captures your close contacts.\n\nThis update requests access to your Location Services for COVIDSafe to work. This permission wakes COVIDSafe up every time you unlock your screen. COVIDSafe does NOT collect, store or use your location data\n\nThis is one of the new ways that COVIDSafe can better capture your close contacts. Find out more about how we’ve improved COVIDSafe";
|
||||
"update_heading" = "Enable Location Services";
|
||||
"update_modal_button" = "Proceed";
|
||||
"upload_answer_no" = "No";
|
||||
/* Upload flow */
|
||||
"upload_answer_yes" = "Yes";
|
||||
"upload_consent_button" = "I agree";
|
||||
"upload_fail_heading" = "Your upload failed";
|
||||
"upload_failed" = "Upload failed";
|
||||
"upload_finished_header" = "Thank you for helping to stop the spread of COVID-19!";
|
||||
"upload_finished_sub_header" = "You have successfully uploaded your information to the COVIDSafe highly secure storage system.\n\nState or territory health officials will notify other COVIDSafe users that have recorded instances of close contact with you. Your identity will remain anonymous to other users.";
|
||||
|
@ -501,7 +507,7 @@
|
|||
"NotificationsDisabled_VOLabel" = "Notifications are disabled. You will not receive a notification if COVIDSafe is not active.\nChange notification settings";
|
||||
"NotificationsEnabled_VOLabel" = "Notifications are enabled. You will receive a notification if COVIDSafe is not active.\nChange notification settings";
|
||||
"OS1b_TopParagraph_VOLabel" = "Bluetooth signals are used to determine when you're near another COVIDSafe user.\nEvery instance of close contact between you and other COVIDSafe users is noted to create close contact information. This information is encrypted and only stored in your phone.\nIf you test positive to COVID-19 as a COVIDSafe user, a state or territory health official will contact you. They will assist with voluntary upload of your close contact information to a highly secure information storage system.\nState or territory health officials can also contact you if you came in close contact with another COVIDSafe user who tested positive.\nFor more information please refer to the Help Topics page.";
|
||||
"permission_content_iOS_2_VOLabel" = "1. Bluetooth\n2. Notifications\n\nCOVIDSafe does not send pairing requests.";
|
||||
"permission_content_iOS_VOLabel" = "COVIDSafe needs Bluetooth enabled to work. By enabling Notifications, you get updates to remind you when COVIDSafe is not active. \n\nSelect 'Proceed' to enable:";
|
||||
"permission_content_iOS_2_VOLabel" = "1. Bluetooth\n2. Notifications\n3. Location Services";
|
||||
"permission_content_iOS_VOLabel" = "COVIDSafe needs Bluetooth enabled to work. COVIDSafe does not send pairing requests.\n\nBy enabling Notifications, you get updates to remind you when COVIDSafe is not active. \n\nYour iPhone requires Location permission for COVIDSafe to work. *COVIDSafe does NOT track or store your location data*\n\nSelect ‘Proceed' to enable:";
|
||||
"ShareCovidSafe_VOLabel" = "Share COVIDSafe. Invite others to help. Together, we're stronger.";
|
||||
"UploadData_VOLabel" = "Has a health official contacted you? You can only upload your information if you have tested positive.";
|
||||
|
|
|
@ -5,4 +5,7 @@
|
|||
"BluetoothUsageDesc" = "COVIDSafe scambia segnali Bluetooth® con cellulari vicini che utilizzano la stessa app. Questi segnali contengono un ID anonimo crittografato che cambia continuamente per garantire la privacy.";
|
||||
"NSBluetoothAlwaysUsageDescription" = "COVIDSafe scambia segnali Bluetooth® con cellulari vicini che utilizzano la stessa app. Questi segnali contengono un ID anonimo crittografato che cambia continuamente per garantire la privacy.";
|
||||
"NSBluetoothPeripheralUsageDescription" = "COVIDSafe scambia segnali Bluetooth® con i cellulari vicini che utilizzano la stessa app. Questi segnali contengono un ID anonimo crittografato che cambia continuamente per garantire la privacy.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "";
|
||||
"NSLocationAlwaysUsageDescription" = "";
|
||||
"NSLocationWhenInUseUsageDescription" = "";
|
||||
"UILaunchStoryboardName" = "LaunchScreen_it";
|
||||
|
|
|
@ -239,7 +239,7 @@
|
|||
/* OnBoarding Data Privacy */
|
||||
"data_privacy_headline" = "Registrazione e privacy";
|
||||
"deaths" = "Decessi";
|
||||
"dialog_error_uploading_message" = "Si è verificato un errore durante il caricamento delle informazioni. Riprova.";
|
||||
"dialog_error_uploading_message" = "";
|
||||
"dialog_error_uploading_negative" = "Annulla";
|
||||
"dialog_error_uploading_positive" = "Riprova";
|
||||
"dialog_uploading_message" = "I dati di COVIDSafe sono attualmente in fase di caricamento. \n\nNon chiudere l'app.";
|
||||
|
@ -317,6 +317,8 @@
|
|||
"jwt_heading" = "Si prega di registrarsi di nuovo";
|
||||
"jwt_success" = "";
|
||||
"loading_numbers" = "Caricamento degli ultimi numeri";
|
||||
"location_off" = "";
|
||||
"location_off_description" = "";
|
||||
/* Splash Screen */
|
||||
"migration_in_progress" = "Aggiornamento COVIDSafe in corso. \n\nAssicurarsi che il cellulare non sia spento fino al completamento dell'aggiornamento.";
|
||||
"national_numbers" = "Numeri nazionali";
|
||||
|
@ -343,9 +345,8 @@
|
|||
"options_for_australia" = "Opzioni per l'Australia";
|
||||
"permission_button" = "Procedi";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS" = "COVIDSafe necessita del Bluetooth® abilitato per funzionare. Abilitando Notifiche, ricevi aggiornamenti per ricordarti quando COVIDSafe non è attivo. \n\nSeleziona 'Procedi' per abilitare:";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS_2" = "1. Bluetooth®\n2. Notifiche\n\nCOVIDSafe non invia richieste di accoppiamento.";
|
||||
"permission_content_iOS" = "";
|
||||
"permission_content_iOS_2" = "";
|
||||
/* OnBoarding Permission */
|
||||
"permission_headline" = "Impostazioni dell'app";
|
||||
"permission_success_button" = "Eseguito";
|
||||
|
@ -412,10 +413,15 @@
|
|||
"update_available_dismiss_btn" = "Ricordamelo più tardi";
|
||||
"update_available_message_ios" = "Abbiamo apportato dei miglioramenti a COVIDSafe. Aggiorna tramite l'App Store";
|
||||
"update_available_title" = "Aggiornamento disponibile!";
|
||||
"update_description" = "";
|
||||
"update_description_VO" = "";
|
||||
"update_heading" = "";
|
||||
"update_modal_button" = "Procedi";
|
||||
"upload_answer_no" = "No";
|
||||
/* Upload flow */
|
||||
"upload_answer_yes" = "Sì";
|
||||
"upload_consent_button" = "Sono d'accordo";
|
||||
"upload_fail_heading" = "";
|
||||
"upload_failed" = "Caricamento non riuscito";
|
||||
"upload_finished_header" = "Grazie per aver aiutato a fermare la diffusione del COVID-19!";
|
||||
"upload_finished_sub_header" = "I vostri dati sono stati caricati sul sistema altamente sicuro di archiviazione di COVIDSafe. \n\nI funzionari sanitari statali o del territorio informeranno gli altri utenti COVIDSafe che hanno registrato casi di stretto contatto con voi. La vostra identità rimarrà anonima per gli altri utenti.";
|
||||
|
@ -501,7 +507,7 @@
|
|||
"NotificationsDisabled_VOLabel" = "Le notifiche sono disabilitate. Non si ricevono notifiche se COVIDSafe non è attivo. \nModifica le impostazioni di notifica";
|
||||
"NotificationsEnabled_VOLabel" = "Le notifiche sono abilitate. Riceverai una notifica se COVIDSafe non è attivo. \nModifica le impostazioni di notifica";
|
||||
"OS1b_TopParagraph_VOLabel" = "I segnali Bluetooth vengono utilizzati per determinare la vicinanza ad altri utenti di COVIDSafe. \nOgni istanza di stretto contatto tra voi e gli altri utenti di COVIDSafe viene annotata per creare informazioni sui contatti. Queste informazioni sono crittografate e memorizzate solo sul vostro cellulare. \nSe risultate positivi al COVID-19, come utenti di COVIDSafe sarete contattati da un funzionario sanitario statale o del territorio per assistervi con il caricamento volontario delle vostre informazioni di stretto contatto in un sistema di archiviazione di informazioni altamente sicuro. \nI funzionari sanitari statali o territoriali possono anche contattarvi se siete entrati in stretto contatto con un altro utente di COVIDSafe risultato positivo. \nPer ulteriori informazioni, consultare la pagina Argomenti della Guida.";
|
||||
"permission_content_iOS_2_VOLabel" = "1. Bluetooth\n2. Notifiche\n\nCOVIDSafe non invia richieste di accoppiamento.";
|
||||
"permission_content_iOS_VOLabel" = "COVIDSafe necessita il Bluetooth abilitato per funzionare. Abilitando Notifiche, ricevete aggiornamenti per sapere quando COVIDSafe non è attivo. \n\n Seleziona 'Procedi' per abilitare:";
|
||||
"permission_content_iOS_2_VOLabel" = "";
|
||||
"permission_content_iOS_VOLabel" = "";
|
||||
"ShareCovidSafe_VOLabel" = "Condividi COVIDSafe. Invita altri ad collaborare. Insieme, siamo più forti.";
|
||||
"UploadData_VOLabel" = "Siete stati contattati da un funzionario sanitario? Potete caricare le vostre informazioni solo se siete risultati positivi.";
|
||||
|
|
|
@ -5,4 +5,7 @@
|
|||
"BluetoothUsageDesc" = "COVIDSafe는 동일한 앱을 실행하는 근처의 휴대폰과 블루투스® 신호를 교환합니다. 이 신호에는 익명의 ID가 포함되어 있으며, 이 ID는 암호화되어 있고 개인정보를 보호하기 위해 지속적으로 변경됩니다.";
|
||||
"NSBluetoothAlwaysUsageDescription" = "COVIDSafe는 동일한 앱을 실행하는 주위의 휴대폰들과 블루투스® 신호들을 교환합니다. 이 신호들에는 익명의 ID가 포함되어 있으며, 이 ID는 암호화되어 있고 귀하의 개인 정보를 보호하기 위해 지속적으로 변경됩니다.";
|
||||
"NSBluetoothPeripheralUsageDescription" = "COVIDSafe는 동일한 앱을 실행하는 주위의 휴대폰들과 블루투스® 신호들을 교환합니다. 이 신호들에는 익명의 ID가 포함되어 있으며, 이 ID는 암호화되어 있고 귀하의 개인 정보를 보호하기 위해 지속적으로 변경됩니다.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "";
|
||||
"NSLocationAlwaysUsageDescription" = "";
|
||||
"NSLocationWhenInUseUsageDescription" = "";
|
||||
"UILaunchStoryboardName" = "LaunchScreen_ko";
|
||||
|
|
|
@ -239,7 +239,7 @@
|
|||
/* OnBoarding Data Privacy */
|
||||
"data_privacy_headline" = "등록 및 개인정보 보호";
|
||||
"deaths" = "사망자";
|
||||
"dialog_error_uploading_message" = "정보를 업로드하는 동안 오류가 발생했습니다. 다시 시도하세요.";
|
||||
"dialog_error_uploading_message" = "";
|
||||
"dialog_error_uploading_negative" = "취소";
|
||||
"dialog_error_uploading_positive" = "다시 시도";
|
||||
"dialog_uploading_message" = "당신의 COVIDSafe 정보가 현재 업로드 중입니다. \n\n앱을 닫지 마세요.";
|
||||
|
@ -317,6 +317,8 @@
|
|||
"jwt_heading" = "다시 등록하십시오";
|
||||
"jwt_success" = "";
|
||||
"loading_numbers" = "최신 번호 로드 중";
|
||||
"location_off" = "";
|
||||
"location_off_description" = "";
|
||||
/* Splash Screen */
|
||||
"migration_in_progress" = "COVIDSafe 업데이트 진행 중. \n\n업데이트가 완료될 때까지 휴대폰이 꺼지지 않도록 해주세요.";
|
||||
"national_numbers" = "전국 수";
|
||||
|
@ -343,9 +345,8 @@
|
|||
"options_for_australia" = "호주 관련 옵션";
|
||||
"permission_button" = "진행";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS" = "COVIDSafe 작동을 위해 블루투스®가 활성화되어 있어야 합니다. 알림기능을 활성화하면, COVIDSafe가 비활성화되어 있을 때 알림을 받게 됩니다. \n\n'진행'을 선택하여 활성화 하십시오:";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS_2" = "1. 블루투스®\n2. 알림\n\nCOVIDSafe는 페어링 요청을 보내지 않습니다.";
|
||||
"permission_content_iOS" = "";
|
||||
"permission_content_iOS_2" = "";
|
||||
/* OnBoarding Permission */
|
||||
"permission_headline" = "앱 설정";
|
||||
"permission_success_button" = "완료";
|
||||
|
@ -412,10 +413,15 @@
|
|||
"update_available_dismiss_btn" = "나중에 다시 알려 주세요";
|
||||
"update_available_message_ios" = "저희는 COVIDSafe의 기능들을 향상시키고 있습니다. 앱 스토어를 통해 업데이트하십시오.";
|
||||
"update_available_title" = "업데이트 가능!";
|
||||
"update_description" = "";
|
||||
"update_description_VO" = "";
|
||||
"update_heading" = "";
|
||||
"update_modal_button" = "진행";
|
||||
"upload_answer_no" = "아니요";
|
||||
/* Upload flow */
|
||||
"upload_answer_yes" = "예";
|
||||
"upload_consent_button" = "동의합니다";
|
||||
"upload_fail_heading" = "";
|
||||
"upload_failed" = "업로드 실패";
|
||||
"upload_finished_header" = "COVID-19의 확산을 막는 데 협조해 주셔서 감사합니다!";
|
||||
"upload_finished_sub_header" = "보안성이 높은 COVIDSafe의 저장 체계에 당신의 정보를 성공적으로 업로드하였습니다. \n\n당신과 가까운 접촉기록이 있는 다른 COVIDSafe 사용자에게 주 또는 테리토리 보건 담당자가 통보할 것입니다. 다른 사용자들에게 당신의 신원은 익명으로 유지됩니다.";
|
||||
|
@ -501,7 +507,7 @@
|
|||
"NotificationsDisabled_VOLabel" = "알림이 꺼졌습니다. COVIDSafe가 활성화되어 있지 않으면 알림을 받을 수 없습니다. \n알림 설정 변경하기";
|
||||
"NotificationsEnabled_VOLabel" = "알림이 켜졌습니다. COVIDSafe가 활성화되어 있지 않으면 알림을 받게 됩니다. 알림 설정 변경하기";
|
||||
"OS1b_TopParagraph_VOLabel" = "블루투스® 신호는 당신이 언제 다른 COVIDSafe 사용자 근처에 있었는지를 확인하는 데 사용됩니다. \n당신과 다른 COVIDSafe 사용자들 사이의 모든 근거리 접촉상황들은 근거리 접촉정보로 기록됩니다. 이 정보는 암호화되어 당신의 휴대폰에만 저장됩니다. \nCOVIDSafe 사용자로서 당신이 COVID-19에 양성 결과가 나오면, 주 또는 테리토리 보건 담당자가 당신에게 연락할 것입니다. 당신이 자신의 근거리 접촉정보를 보안성 높은 정보 저장 체계에 자발적으로 업로드할 수 있도록 도와줄 것입니다. \n또한 양성 결과가 나온 다른 COVIDSafe 사용자와 당신이 가까이 접촉한 경우, 주 또는 테리토리 보건 담당자가 당신에게 연락을 취할 수 있습니다. \n자세한 내용은 *도움말 항목* 페이지를 참조하세요.";
|
||||
"permission_content_iOS_2_VOLabel" = "1. 블루투스\n2. 알림\n\nCOVIDSafe는 페어링 요청을 보내지 않습니다.";
|
||||
"permission_content_iOS_VOLabel" = "COVIDSafe가 작동하려면 블루투스가 활성화되어 있어야 합니다. 알림을 켜면 COVIDSafe가 활성화되지 않았음을 알리는 업데이트를 받게 됩니다. \n\n '진행'을 선택하여 다음을 활성화 하십시오.";
|
||||
"permission_content_iOS_2_VOLabel" = "";
|
||||
"permission_content_iOS_VOLabel" = "";
|
||||
"ShareCovidSafe_VOLabel" = "COVIDSafe를 공유하세요. 다른 이들에게 참여를 권유하세요. 우리는 함께할 때 더 강해집니다.";
|
||||
"UploadData_VOLabel" = "보건 담당자가 여러분에게 연락했습니까? 양성 검사 결과를 받은 경우에만 여러분의 정보를 업로드할 수 있습니다.";
|
||||
|
|
|
@ -5,4 +5,7 @@
|
|||
"BluetoothUsageDesc" = "COVIDSafe ਨਜ਼ਦੀਕੀ ਇਕੋ ਜਿਹੀ ਐਪ ਵਾਲੇ ਫੋਨਾਂ ਨਾਲ ਬਲੂਟੁੱਥ® ਸਿਗਨਲਾਂ ਦਾ ਵਟਾਂਦਰਾ ਕਰਦਾ ਹੈ। ਇਹਨਾਂ ਸਿਗਨਲਾਂ ਵਿੱਚ ਇੱਕ ਗੁੰਮਨਾਮ ਏਨਕ੍ਰਿਪਟਿਡ ਆਈ.ਡੀ. ਹੁੰਦੀ ਹੈ ਜੋ ਤੁਹਾਡੀ ਪ੍ਰਾਈਵੇਸੀ ਨੂੰ ਯਕੀਨੀ ਬਣਾਉਣ ਲਈ ਲਗਾਤਾਰ ਬਦਲਦੀ ਰਹਿੰਦੀ ਹੈ|";
|
||||
"NSBluetoothAlwaysUsageDescription" = "COVIDSafe ਨਜ਼ਦੀਕੀ ਇਕੋ ਜਿਹੀ ਐਪ ਵਾਲੇ ਫੋਨਾਂ ਨਾਲ Bluetooth ਸਿਗਨਲਾਂ ਦਾ ਵਟਾਂਦਰਾ ਕਰਦਾ ਹੈ। ਇਹਨਾਂ ਸਿਗਨਲਾਂ ਵਿੱਚ ਇੱਕ ਗੁੰਮਨਾਮ ਏਨਕ੍ਰਿਪਟਿਡ ਆਈ.ਡੀ. ਹੁੰਦੀ ਹੈ ਜੋ ਤੁਹਾਡੀ ਪ੍ਰਾਈਵੇਸੀ ਨੂੰ ਯਕੀਨੀ ਬਣਾਉਣ ਲਈ ਲਗਾਤਾਰ ਬਦਲਦੀ ਰਹਿੰਦੀ ਹੈ|";
|
||||
"NSBluetoothPeripheralUsageDescription" = "CovidSafe ਨਜ਼ਦੀਕੀ ਇਕੋ ਜਿਹੀ ਐਪ ਵਾਲੇ ਫੋਨਾਂ ਨਾਲ Bluetooth ਸਿਗਨਲਾਂ ਦਾ ਵਟਾਂਦਰਾ ਕਰਦਾ ਹੈ। ਇਹਨਾਂ ਸਿਗਨਲਾਂ ਵਿੱਚ ਇੱਕ ਗੁੰਮਨਾਮ ਏਨਕ੍ਰਿਪਟਿਡ ਆਈ.ਡੀ. ਹੁੰਦੀ ਹੈ ਜੋ ਤੁਹਾਡੀ ਪ੍ਰਾਈਵੇਸੀ ਨੂੰ ਯਕੀਨੀ ਬਣਾਉਣ ਲਈ ਲਗਾਤਾਰ ਬਦਲਦੀ ਰਹਿੰਦੀ ਹੈ|";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "";
|
||||
"NSLocationAlwaysUsageDescription" = "";
|
||||
"NSLocationWhenInUseUsageDescription" = "";
|
||||
"UILaunchStoryboardName" = "LaunchScreen_pa-IN";
|
||||
|
|
|
@ -239,7 +239,7 @@
|
|||
/* OnBoarding Data Privacy */
|
||||
"data_privacy_headline" = "ਪੰਜੀਕਰਨ ਅਤੇ ਪ੍ਰਾਈਵੇਸੀ";
|
||||
"deaths" = "ਮੌਤਾਂ";
|
||||
"dialog_error_uploading_message" = "ਤੁਹਾਡੀ ਜਾਣਕਾਰੀ ਨੂੰ ਅੱਪਲੋਡ ਕਰਨ ਦੌਰਾਨ ਇੱਕ ਗਲਤੀ ਆਈ, ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।";
|
||||
"dialog_error_uploading_message" = "";
|
||||
"dialog_error_uploading_negative" = "ਰੱਦ";
|
||||
"dialog_error_uploading_positive" = "ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ";
|
||||
"dialog_uploading_message" = "ਤੁਹਾਡੀ COVIDSafe ਜਾਣਕਾਰੀ ਨੂੰ ਇਸ ਵੇਲੇ ਅੱਪਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ।\n\nਕਿਰਪਾ ਕਰਕੇ ਐਪ ਬੰਦ ਨਾ ਕਰੋ।";
|
||||
|
@ -315,8 +315,10 @@
|
|||
"invalid_norfolk_island_phone_number_error_prompt" = "ਨੋਰਫੋਕ ਆਈਲੈਂਡਜ਼ ਵਿਖੇ ਮੋਬਾਈਲ ਨੰਬਰਾਂ ਦੇ 5 ਤੋਂ 6 ਅੰਕ ਹੁੰਦੇ ਹਨ।";
|
||||
"jwt_description" = "ਤੁਹਾਡੇ ਪੰਜੀਕਰਨ (ਰਜ਼ਿਸਟ੍ਰੇਸ਼ਨ) ਦੇ ਵਿਸਥਾਰਾਂ ਵਿੱਚ ਕੋਈ ਸਮੱਸਿਆ ਹੈ।";
|
||||
"jwt_heading" = "ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਰਜਿਸਟਰ ਕਰੋ";
|
||||
"jwt_success" = "";
|
||||
"jwt_success" = "ਰਜਿਸਟਰੇਸ਼ਨ ਸਫਲਤਾਪੂਰਵਕ ਰਿਨਿਯੂ ਕੀਤਾ ਗਿਆ";
|
||||
"loading_numbers" = "ਤਾਜ਼ਾ ਅੰਕੜੇ ਲੋਡ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ";
|
||||
"location_off" = "";
|
||||
"location_off_description" = "";
|
||||
/* Splash Screen */
|
||||
"migration_in_progress" = "COVIDSafe ਅੱਪਡੇਟ ਚੱਲ ਰਿਹਾ ਹੈ। \n\nਕਿਰਪਾ ਕਰਕੇ ਯਕੀਨੀ ਬਣਾਓ ਕਿ ਤੁਹਾਡਾ ਫ਼ੋਨ ਤਦ ਤੱਕ ਬੰਦ ਨਹੀਂ ਹੋਣਾ ਚਾਹੀਦਾ ਜਦ ਤੱਕ ਅੱਪਡੇਟ ਪੂਰਾ ਨਹੀਂ ਹੋ ਜਾਂਦਾ।";
|
||||
"national_numbers" = "ਰਾਸ਼ਟਰੀ ਅੰਕੜੇ";
|
||||
|
@ -343,9 +345,8 @@
|
|||
"options_for_australia" = "ਆਸਟਰੇਲੀਆ ਲਈ ਵਿਕਲਪ";
|
||||
"permission_button" = "ਅੱਗੇ ਵਧੋ";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS" = "COVIDSafe ਲਈ ਬਲੂਟੁਥ ਅਨੇਬਲ ਕਰੋ| ਸੂਚਨਾਵਾਂ ਨੂੰ ਅਨੇਬਲ ਕਰਣ ਤੇ ਤੁਹਾਨੂੰ ਅੱਪਡੇਟ ਪ੍ਰਾਪਤ ਹੁੰਦੇ ਹਨ ਜੋ ਯਾਦ ਕਰਾਉਂਦੇ ਹਨ ਜਦੋਂ COVIDSafe ਐਕਟਿਵ ਨਹੀਂ ਹੁੰਦਾ|\n\nਚਲਾਉਣ ਲਈ 'ਅੱਗੇ ਵਧੋ' ਚੁਣੋ:";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS_2" = "1. Bluetooth®\n2. ਸੂਚਨਾਵਾਂ\n\nCOVIDSafe ਪੇਅਰਿੰਗ ਦੀਆਂ ਬੇਨਤੀਆਂ ਨਹੀਂ ਭੇਜਦੀ।";
|
||||
"permission_content_iOS" = "";
|
||||
"permission_content_iOS_2" = "";
|
||||
/* OnBoarding Permission */
|
||||
"permission_headline" = "ਐਪ ਸੈਟਿੰਗਾਂ";
|
||||
"permission_success_button" = "ਕੀਤਾ";
|
||||
|
@ -412,10 +413,15 @@
|
|||
"update_available_dismiss_btn" = "ਮੈਨੂੰ ਬਾਅਦ ਵਿੱਚ ਯਾਦ ਕਰਵਾਓ";
|
||||
"update_available_message_ios" = "ਅਸੀਂ COVIDSafe ਵਿੱਚ ਸੁਧਾਰ ਕਰਦੇ ਆ ਰਹੇ ਹਾਂ| ਐਪ ਸਟੋਰ ਰਾਹੀਂ ਅਪਡੇਟ ਕਰੋ।";
|
||||
"update_available_title" = "ਅੱਪਡੇਟ ਉਪਲਬਧ ਹੈ|";
|
||||
"update_description" = "";
|
||||
"update_description_VO" = "";
|
||||
"update_heading" = "";
|
||||
"update_modal_button" = "ਅੱਗੇ ਵਧੋ";
|
||||
"upload_answer_no" = "ਨਹੀਂ";
|
||||
/* Upload flow */
|
||||
"upload_answer_yes" = "ਹਾਂ";
|
||||
"upload_consent_button" = "ਮੈਂ ਸਹਿਮਤ ਹਾਂ";
|
||||
"upload_fail_heading" = "";
|
||||
"upload_failed" = "ਅੱਪਲੋਡ ਅਸਫਲ";
|
||||
"upload_finished_header" = "COVIDSafe ਦੇ ਫੈਲਾਅ ਨੂੰ ਰੋਕਣ ਵਿੱਚ ਸਹਾਇਤਾ ਲਈ ਤੁਹਾਡਾ ਧੰਨਵਾਦ!";
|
||||
"upload_finished_sub_header" = "ਤੁਸੀਂ ਆਪਣੀ ਜਾਣਕਾਰੀ ਨੂੰ COVIDSafe ਦੇ ਬੇਹੱਦ ਸੁਰੱਖਿਅਤ ਸਟੋਰੇਜ ਸਿਸਟਮ 'ਤੇ ਸਫਲਤਾ ਪੂਰਵਕ ਅੱਪਲੋਡ ਕਰ ਦਿੱਤਾ ਹੈ।\n\nਰਾਜ ਜਾਂ ਹਲਕਾ ਸਿਹਤ ਅਧਿਕਾਰੀ ਹੋਰ COVIDSafe ਵਰਤੋਂਕਾਰਾਂ ਨੂੰ ਸੂਚਿਤ ਕਰਨਗੇ ਜਿੰਨ੍ਹਾਂ ਨੇ ਤੁਹਾਡੇ ਨਾਲ ਨਜ਼ਦੀਕੀ ਸੰਪਰਕ ਦੀਆਂ ਉਦਾਹਰਨਾਂ ਰਿਕਾਰਡ ਕੀਤੀਆਂ ਹਨ। ਤੁਹਾਡੀ ਪਛਾਣ ਹੋਰਨਾਂ ਵਰਤੋਂਕਾਰਾਂ ਵਾਸਤੇ ਗੁੰਮਨਾਮ ਰਹੇਗੀ।";
|
||||
|
@ -501,7 +507,7 @@
|
|||
"NotificationsDisabled_VOLabel" = "ਸੂਚਨਾਵਾਂ ਬੰਦ (ਡਿਸੇਬਲੇਡ) ਹਨ। ਜੇ COVIDSafe ਐਕਟਿਵ ਨਹੀਂ ਤਾਂ ਤੁਹਾਨੂੰ ਕੋਈ ਸੂਚਨਾ ਨਹੀਂ ਮਿਲੇਗੀ|\nਸੂਚਨਾ ਸੈਟਿੰਗਜ਼ ਨੂੰ ਬਦਲੋ";
|
||||
"NotificationsEnabled_VOLabel" = "ਸੂਚਨਾਵਾਂ ਚਾਲੂ (ਅਨੇਬਲਡ) ਹਨ। ਜੇ COVIDSafe ਐਕਟਿਵ ਨਹੀਂ ਤਾਂ ਤੁਹਾਨੂੰ ਸੂਚਨਾ ਮਿਲੇਗੀ|\nਸੂਚਨਾ ਸੈਟਿੰਗਜ਼ ਨੂੰ ਬਦਲੋ";
|
||||
"OS1b_TopParagraph_VOLabel" = "ਬਲੂਟੁੱਥ ਸਿਗਨਲਾਂ ਦੀ ਵਰਤੋਂ ਇਹ ਦੱਸਦੀ ਹੈ ਜਦੋਂ ਤੁਸੀਂ ਕਿਸੇ ਹੋਰ COVIDSafe ਵਰਤੋਂਕਾਰ ਦੇ ਨੇੜੇ ਹੁੰਦੇ ਹੋ।\nਤੁਹਾਡੇ ਅਤੇ ਹੋਰ COVIDSafe ਵਰਤੋਂਕਾਰਾਂ ਵਿਚਕਾਰ ਨਜ਼ਦੀਕੀ ਸੰਪਰਕ ਦੀ ਹਰੇਕ ਉਦਾਹਰਨ ਨੂੰ ਨਜ਼ਦੀਕੀ ਸੰਪਰਕ ਜਾਣਕਾਰੀ ਲਈ ਨੋਟ ਕੀਤਾ ਜਾਂਦਾ ਹੈ। ਇਹ ਜਾਣਕਾਰੀ ਏਨਕ੍ਰਿਪਟਿਡ ਹੁੰਦੀ ਹੈ ਅਤੇ ਕੇਵਲ ਤੁਹਾਡੇ ਫ਼ੋਨ ਵਿੱਚ ਹੀ ਸਟੋਰ ਕੀਤੀ ਜਾਂਦੀ ਹੈ।\nਜੇ COVIDSafe ਵਰਤੋਂਕਾਰ ਵਜੋਂ ਕੋਵਿਡ-19 ਲਈ ਤੁਹਾਡਾ ਟੈਸਟ ਪਾਜੇਟਿਵ ਆਉਂਦਾ ਹੈ, ਤਾਂ ਕੋਈ ਰਾਜ ਜਾਂ ਹਲਕਾ ਸਿਹਤ ਅਧਿਕਾਰੀ ਤੁਹਾਡੇ ਨਾਲ ਸੰਪਰਕ ਕਰੇਗਾ। ਉਹ ਤੁਹਾਡੀ ਨਜ਼ਦੀਕੀ ਸੰਪਰਕ ਜਾਣਕਾਰੀ ਨੂੰ ਇੱਕ ਬੇਹੱਦ ਸੁਰੱਖਿਅਤ ਜਾਣਕਾਰੀ ਸਟੋਰੇਜ ਸਿਸਟਮ ਵਿੱਚ ਅੱਪਲੋਡ ਕਰਨ ਵਿੱਚ ਸਹਾਇਤਾ ਕਰਨਗੇ।\nਜੇ ਤੁਸੀਂ ਕਿਸੇ ਹੋਰ COVIDSafe ਵਰਤੋਂਕਾਰ ਜਿਸਦਾ ਪਾਜ਼ੇਟਿਵ ਟੈਸਟ ਹੋਇਆ ਸੀ, ਦੇ ਸੰਪਰਕ ਵਿੱਚ ਆਏ ਹੋ ਤਾਂ ਵੀ ਰਾਜ ਜਾਂ ਹਲਕਾ ਸਿਹਤ ਅਧਿਕਾਰੀ ਤੁਹਾਡੇ ਨਾਲ ਸੰਪਰਕ ਕਰ ਸਕਦੇ ਹਨ|\nਵਧੇਰੇ ਜਾਣਕਾਰੀ ਲਈ ਕਿਰਪਾ ਕਰਕੇ ਸਹਾਇਤਾ ਵਿਸ਼ੇ ਪੰਨਾ ਦੇਖੋ।";
|
||||
"permission_content_iOS_2_VOLabel" = "1. ਬਲੂਟੁੱਥ\n2. ਸੂਚਨਾਵਾਂ\n\nCOVIDSafe ਪੇਅਰਿੰਗ ਦੀਆਂ ਬੇਨਤੀਆਂ ਨਹੀਂ ਭੇਜਦੀ।";
|
||||
"permission_content_iOS_VOLabel" = "COVIDSafe ਦੇ ਕੰਮ ਕਰਨ ਲਈ ਬਲੂਟੁਥ ਚਾਲੂ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ| ਸੂਚਨਾਵਾਂ ਨੂੰ ਚਾਲੂ ਕਰਣ ਤੇ ਤੁਹਾਨੂੰ ਅੱਪਡੇਟ ਪ੍ਰਾਪਤ ਹੁੰਦੇ ਹਨ ਜੋ ਯਾਦ ਕਰਾਉਂਦੇ ਹਨ ਜਦੋਂ COVIDSafe ਐਕਟਿਵ ਨਹੀਂ ਹੁੰਦਾ|\n\nਚਲਾਉਣ ਲਈ 'ਅੱਗੇ ਵਧੋ' ਚੁਣੋ:";
|
||||
"permission_content_iOS_2_VOLabel" = "";
|
||||
"permission_content_iOS_VOLabel" = "";
|
||||
"ShareCovidSafe_VOLabel" = "COVIDSafe ਸਾਂਝਾ ਕਰੋ। ਹੋਰਨਾਂ ਨੂੰ ਸਹਾਇਤਾ ਲਈ ਬੁਲਾਓ । ਇਕੱਠੇ ਅਸੀਂ ਵਧੇਰੇ ਮਜ਼ਬੂਤ ਹਾਂ।";
|
||||
"UploadData_VOLabel" = "ਕੀ ਕਿਸੇ ਸਿਹਤ ਅਧਿਕਾਰੀ ਨੇ ਤੁਹਾਡੇ ਨਾਲ ਸੰਪਰਕ ਕੀਤਾ ਹੈ? ਤੁਸੀਂ ਆਪਣੀ ਜਾਣਕਾਰੀ ਨੂੰ ਕੇਵਲ ਤਾਂ ਹੀ ਅੱਪਲੋਡ ਕਰ ਸਕਦੇ ਹੋ ਜੇਕਰ ਤੁਹਾਡਾ ਟੈਸਟ ਪਾਜ਼ੇਟਿਵ ਆਇਆ ਹੋਵੇ|";
|
||||
|
|
73
CovidSafe/staging-Info.plist
Normal file
73
CovidSafe/staging-Info.plist
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>GitBranchInfo</key>
|
||||
<string>feature/api_auth_int - Commit: 43b7ab0</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>COVIDSafe exchanges Bluetooth® signals with nearby phones running the same app. These signals contain an anonymised ID, which is encrypted and changes continually to ensure your privacy.</string>
|
||||
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||
<string>COVIDSafe exchanges Bluetooth® signals with nearby phones running the same app. These signals contain an anonymised ID, which is encrypted and changes continually to ensure your privacy.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>COVIDSafe exchanges Bluetooth® signals with nearby phones running the same app. Location permission is required for best performance, your location is not recorded to ensure your privacy.</string>
|
||||
<key>NSLocationAlwaysUsageDescription</key>
|
||||
<string>COVIDSafe exchanges Bluetooth® signals with nearby phones running the same app. Location permission is required for best performance, your location is not recorded to ensure your privacy.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>COVIDSafe exchanges Bluetooth® signals with nearby phones running the same app. Location permission is required for best performance, your location is not recorded to ensure your privacy.</string>
|
||||
<key>TRACER_SVC_ID</key>
|
||||
<string>${SERVICE_UUID}</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>bluetooth-central</string>
|
||||
<string>bluetooth-peripheral</string>
|
||||
<string>location</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen_en</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -5,4 +5,7 @@
|
|||
"BluetoothUsageDesc" = "COVIDSafe, aynı uygulamayı çalıştıran yakındaki telefonlarla Bluetooth® sinyalleri alışverişinde bulunur. Bu sinyaller, gizliliğinizi sağlamak için şifrelenen ve sürekli değişen anonimleştirilmiş bir kimlik içerir.";
|
||||
"NSBluetoothAlwaysUsageDescription" = "COVIDSafe, aynı uygulamayı çalıştıran yakındaki telefonlarla Bluetooth® sinyalleri alışverişinde bulunur. Bu sinyaller, gizliliğinizi sağlamak için şifrelenen ve sürekli değişen anonimleştirilmiş bir kimlik içerir.";
|
||||
"NSBluetoothPeripheralUsageDescription" = "COVIDSafe, aynı uygulamayı çalıştıran yakındaki telefonlarla Bluetooth® sinyalleri alışverişinde bulunur. Bu sinyaller, gizliliğinizi sağlamak için şifrelenen ve sürekli değişen anonimleştirilmiş bir kimlik içerir.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "";
|
||||
"NSLocationAlwaysUsageDescription" = "";
|
||||
"NSLocationWhenInUseUsageDescription" = "";
|
||||
"UILaunchStoryboardName" = "LaunchScreen_tr";
|
||||
|
|
|
@ -239,7 +239,7 @@
|
|||
/* OnBoarding Data Privacy */
|
||||
"data_privacy_headline" = "Kayıt ve gizlilik";
|
||||
"deaths" = "Ölümler";
|
||||
"dialog_error_uploading_message" = "Bilgileriniz sisteme yüklerken bir hata oluştu, lütfen tekrar deneyin.";
|
||||
"dialog_error_uploading_message" = "";
|
||||
"dialog_error_uploading_negative" = "İptal";
|
||||
"dialog_error_uploading_positive" = "Tekrar deneyin";
|
||||
"dialog_uploading_message" = "COVIDSafe bilgileriniz şu anda sisteme yükleniyor. \n\n Lütfen uygulamayı kapatmayın.";
|
||||
|
@ -317,6 +317,8 @@
|
|||
"jwt_heading" = "Lütfen tekrar kayıt olun";
|
||||
"jwt_success" = "";
|
||||
"loading_numbers" = "En son sayılar yükleniyor";
|
||||
"location_off" = "";
|
||||
"location_off_description" = "";
|
||||
/* Splash Screen */
|
||||
"migration_in_progress" = " COVIDSafe güncellemesi devam ediyor. \n\n Lütfen güncelleme tamamlanana kadar telefonunuzu kapatmayın.";
|
||||
"national_numbers" = "Ulusal sayılar";
|
||||
|
@ -343,9 +345,8 @@
|
|||
"options_for_australia" = "Avustralya için seçenekler";
|
||||
"permission_button" = "İlerle";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS" = "COVIDSafe'nin çalışması için Bluetooth® etkinleştirilmiş olmalıdır. Bildirimleri etkinleştirerek COVIDSafe aktif olmadığında size hatırlatan güncellemeleri alabilirsiniz. \n\nEtkinleştirmek için 'Devam Et'i seçin:";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS_2" = "1. Bluetooth®\n2. Bildirimler \n\nCOVIDSafe eşleştirme istekleri göndermez.";
|
||||
"permission_content_iOS" = "";
|
||||
"permission_content_iOS_2" = "";
|
||||
/* OnBoarding Permission */
|
||||
"permission_headline" = "Uygulama ayarları";
|
||||
"permission_success_button" = "Bitti";
|
||||
|
@ -412,10 +413,15 @@
|
|||
"update_available_dismiss_btn" = "Daha sonra hatırlat";
|
||||
"update_available_message_ios" = "COVIDSafe'de iyileştirmeler yapmaktayız. App Store üzerinden güncelleme yapın.";
|
||||
"update_available_title" = "Güncelleme mevcut!";
|
||||
"update_description" = "";
|
||||
"update_description_VO" = "";
|
||||
"update_heading" = "";
|
||||
"update_modal_button" = "İlerle";
|
||||
"upload_answer_no" = "Hayır";
|
||||
/* Upload flow */
|
||||
"upload_answer_yes" = "Evet";
|
||||
"upload_consent_button" = "Kabul ediyorum";
|
||||
"upload_fail_heading" = "";
|
||||
"upload_failed" = "Karşıya yükleme başarısız";
|
||||
"upload_finished_header" = "COVID-19'un yayılmasını durdurmaya yardım ettiğiniz için teşekkür ederiz!";
|
||||
"upload_finished_sub_header" = "Bilgilerinizi yüksek güvenlikli COVIDSafe depolama sistemine başarıyla yüklediniz. \n\nEyalet veya bölge sağlık yetkilileri sizinle yakın temas durumlarını kaydeden diğer COVIDSafe kullanıcılarını bilgilendirecektir. Kimliğiniz diğer kullanıcılar için gizli kalacaktır.";
|
||||
|
@ -501,7 +507,7 @@
|
|||
"NotificationsDisabled_VOLabel" = "Bildirimler devre dışı bırakıldı. COVIDSafe aktif olmadığında bildirim almayacaksınız. \nBildirim ayarlarını değiştirin";
|
||||
"NotificationsEnabled_VOLabel" = "Bildirimler etkinleştirildi. COVIDSafe aktif olmadığı durumda bildirim alacaksınız. \nBildirim ayarlarını değiştirin";
|
||||
"OS1b_TopParagraph_VOLabel" = "Bluetooth sinyalleri başka bir COVIDSafe kullanıcısının yakınınızda olduğunuzu belirlemek için kullanılır. \nSizinle diğer COVIDSafe kullanıcıları arasındaki her yakın temas, yakın temas bilgileri oluşturmak için not edilir. Bu bilgiler şifrelenir ve yalnızca telefonunuzda saklanır. \nCOVIDSafe kullanıcısı olarak COVID-19 testinizin pozitif çıkması durumunda eyalet veya bölge sağlık yetkilisi sizinle iletişime geçecektir. Yakın iletişim bilgilerinizin son derece güvenli bir bilgi depolama sistemine gönüllü olarak yüklemenize yardımcı olacaklardır. \nEyalet veya bölge sağlık yetkilileri testi pozitif çıkan başka bir COVIDSafe kullanıcısıyla yakın temas kurdugunuz durumda yine sizinle iletişime geçebilir. \nDaha fazla bilgi için lütfen Yardım Konuları sayfasına bakınız.";
|
||||
"permission_content_iOS_2_VOLabel" = "1. Bluetooth\n2. Bildirimler \n\nCOVIDSafe eşleştirme istekleri göndermez.";
|
||||
"permission_content_iOS_VOLabel" = "COVIDSafe'nin çalışabilmesi için Bluetooth etkinleştirilmiş olmalıdır. Bildirimleri etkinleştirerek COVIDSafe aktif olmadığında size hatırlatan güncellemeleri alabilirsiniz. \n\nEtkinleştirmek için 'Devam Et'i seçiniz:";
|
||||
"permission_content_iOS_2_VOLabel" = "";
|
||||
"permission_content_iOS_VOLabel" = "";
|
||||
"ShareCovidSafe_VOLabel" = "COVIDSafe'i paylaşın. Başkalarını yardıma davet edin. Birlikte daha da güçlüyüz.";
|
||||
"UploadData_VOLabel" = "Bir sağlık yetkilisi sizinle iletişime geçti mi? Bilgilerinizi yalnızca testiniz pozitifse sisteme yükleyebilirsiniz.";
|
||||
|
|
|
@ -5,4 +5,7 @@
|
|||
"BluetoothUsageDesc" = "COVIDSafe trao đổi tín hiệu Bluetooth® với các điện thoại chạy cùng ứng dụng xung quanh. Các tín hiệu này chứa mã ID ẩn danh, được mã hóa và thay đổi liên tục để đảm bảo quyền riêng tư của bạn.";
|
||||
"NSBluetoothAlwaysUsageDescription" = "COVIDSafe trao đổi tín hiệu Bluetooth® với các điện thoại chạy cùng ứng dụng xung quanh. Các tín hiệu này chứa ID ẩn danh, được mã hóa và thay đổi liên tục để đảm bảo quyền riêng tư của bạn.";
|
||||
"NSBluetoothPeripheralUsageDescription" = "COVIDSafe trao đổi tín hiệu Bluetooth® với các điện thoại chạy cùng ứng dụng xung quanh. Các tín hiệu này chứa ID ẩn danh, được mã hóa và thay đổi liên tục để đảm bảo quyền riêng tư của bạn.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "";
|
||||
"NSLocationAlwaysUsageDescription" = "";
|
||||
"NSLocationWhenInUseUsageDescription" = "";
|
||||
"UILaunchStoryboardName" = "LaunchScreen_vi";
|
||||
|
|
|
@ -239,7 +239,7 @@
|
|||
/* OnBoarding Data Privacy */
|
||||
"data_privacy_headline" = "Đăng ký và bảo mật";
|
||||
"deaths" = "Tử vong";
|
||||
"dialog_error_uploading_message" = "Bị lỗi trong khi đăng tải thông tin của bạn, vui lòng thử lại.";
|
||||
"dialog_error_uploading_message" = "";
|
||||
"dialog_error_uploading_negative" = "Hủy bỏ";
|
||||
"dialog_error_uploading_positive" = "Thử lại";
|
||||
"dialog_uploading_message" = "Thông tin COVIDSafe của bạn hiện đang được đăng tải. \n\n Vui lòng không đóng ứng dụng.";
|
||||
|
@ -247,7 +247,7 @@
|
|||
"done_success" = "Hoàn tất";
|
||||
"enter_number_button" = "Nhận mã PIN";
|
||||
"enter_number_content" = "Chúng tôi sẽ gửi cho bạn mã PIN gồm 6 chữ số để xác minh số điện thoại của bạn.";
|
||||
"enter_number_headline" = "Nhập sô điện thoại di động của bạn";
|
||||
"enter_number_headline" = "Nhập số điện thoại di động của bạn";
|
||||
"enter_number_relative" = "Đăng ký dùm bạn bè hoặc người thân? \n\nHọ cần phải đăng ký bằng máy điện thoại và số điện thoại của riêng họ để COVIDSafe có thể hoạt động cho họ.";
|
||||
"enter_pin_button" = "Xác minh";
|
||||
/* OnBoarding Enter PIN */
|
||||
|
@ -317,6 +317,8 @@
|
|||
"jwt_heading" = "Vui lòng đăng ký lại";
|
||||
"jwt_success" = "";
|
||||
"loading_numbers" = "Đang tải các số mới nhất";
|
||||
"location_off" = "";
|
||||
"location_off_description" = "";
|
||||
/* Splash Screen */
|
||||
"migration_in_progress" = "COVIDSafe trong tiến trình cập nhật. \n\nVui lòng đảm bảo điện thoại của bạn không bị tắt cho đến khi cập nhật hoàn tất.";
|
||||
"national_numbers" = "Số ca nhiễm trên toàn quốc";
|
||||
|
@ -343,9 +345,8 @@
|
|||
"options_for_australia" = "Tùy chọn cho Úc";
|
||||
"permission_button" = "Kích hoạt";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS" = "Cần bật Bluetooth® để COVIDSafe hoạt động. Khi bật Thông báo, bạn nhận được các cập nhật để nhắc nhở khi COVIDSafe không hoạt động. \n\n Chọn 'Kích hoạt' để bật:";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS_2" = "1. Bluetooth® \n2. Thông báo \n\n COVIDSafe không gửi yêu cầu kết nối.";
|
||||
"permission_content_iOS" = "";
|
||||
"permission_content_iOS_2" = "";
|
||||
/* OnBoarding Permission */
|
||||
"permission_headline" = "Cài đặt ứng dụng";
|
||||
"permission_success_button" = "Hoàn tất";
|
||||
|
@ -412,10 +413,15 @@
|
|||
"update_available_dismiss_btn" = "Nhắc tôi sau";
|
||||
"update_available_message_ios" = "Chúng tôi đã và đang cải thiện COVIDSafe. Cập nhật qua Kho Ứng dụng (App Store).";
|
||||
"update_available_title" = "Cập nhật có sẵn!";
|
||||
"update_description" = "";
|
||||
"update_description_VO" = "";
|
||||
"update_heading" = "";
|
||||
"update_modal_button" = "Kích hoạt";
|
||||
"upload_answer_no" = "Không";
|
||||
/* Upload flow */
|
||||
"upload_answer_yes" = "Có";
|
||||
"upload_consent_button" = "Tôi đồng ý";
|
||||
"upload_fail_heading" = "";
|
||||
"upload_failed" = "Đăng tải thất bại";
|
||||
"upload_finished_header" = "Cảm ơn bạn đã giúp ngăn chặn sự lây lan của COVID-19!";
|
||||
"upload_finished_sub_header" = "Bạn đã đăng tải thành công thông tin của mình vào hệ thống lưu trữ bảo mật cao của COVIDSafe. \n\nCác nhân viên y tế của tiểu bang hoặc lãnh thổ sẽ thông báo cho những người sử dụng COVIDSafe khác khi tiếp xúc gần với bạn. Danh tính của bạn sẽ được ẩn danh đối với người sử dụng khác.";
|
||||
|
@ -501,7 +507,7 @@
|
|||
"NotificationsDisabled_VOLabel" = "Thông báo bị vô hiệu hóa. Bạn sẽ không nhận được thông báo nếu COVIDSafe không hoạt động. \nThay đổi cài đặt thông báo";
|
||||
"NotificationsEnabled_VOLabel" = "Thông báo được kích hoạt. Bạn sẽ nhận được thông báo nếu COVIDSafe không hoạt động. \nThay đổi cài đặt thông báo";
|
||||
"OS1b_TopParagraph_VOLabel" = "Tín hiệu Bluetooth được sử dụng để xác định khi bạn ở gần người dùng COVIDSafe khác. \nMọi trường hợp tiếp xúc gần giữa bạn và những người dùng COVIDSafe khác đều được ghi nhận để tạo thông tin về mối tiếp xúc gần. Thông tin này sẽ được mã hóa và chỉ lưu trữ trong điện thoại của bạn. \nNếu bạn xét nghiệm dương tính với COVID-19 và là người dùng COVIDSafe, nhân viên y tế của tiểu bang hoặc lãnh thổ sẽ liên hệ với bạn. Họ sẽ tự nguyện hỗ trợ đăng tải thông tin về tiếp xúc gần của bạn vào hệ thống lưu trữ thông tin có độ an toàn cao \nCác nhân viên y tế của tiểu bang hoặc lãnh thổ cũng có thể liên hệ với bạn nếu bạn tiếp xúc gần với một người dùng COVIDSafe khác có kết quả xét nghiệm dương tính. \nĐể biết thêm thông tin, vui lòng tham khảo trang Chủ đề Trợ giúp";
|
||||
"permission_content_iOS_2_VOLabel" = "1. Bluetooth \n2. Thông báo \n\n COVIDSafe không gửi yêu cầu ghép nối.";
|
||||
"permission_content_iOS_VOLabel" = "Cần bật Bluetooth để COVIDSafe hoạt động. Khi bật Thông báo, bạn nhận được các cập nhật để nhắc nhở khi COVIDSafe không hoạt động. \n\n Chọn 'Kích hoạt' để bật:";
|
||||
"permission_content_iOS_2_VOLabel" = "";
|
||||
"permission_content_iOS_VOLabel" = "";
|
||||
"ShareCovidSafe_VOLabel" = "Chia sẻ COVIDSafe. Mời người khác giúp đỡ. Cùng nhau, chúng ta mạnh mẽ hơn.";
|
||||
"UploadData_VOLabel" = "Nhân viên y tế có liên lạc với bạn không? Bạn chỉ có thể đăng tải thông tin của mình nếu bạn có kết quả xét nghiệm dương tính.";
|
||||
|
|
|
@ -5,4 +5,7 @@
|
|||
"BluetoothUsageDesc" = "COVIDSafe与附近运行同一应用程序的手机交换蓝牙®信号。这些信号包含一组匿名ID,该ID经过加密并不断更改以保护您的隐私。";
|
||||
"NSBluetoothAlwaysUsageDescription" = "COVIDSafe与附近运行同一应用程序的手机交换蓝牙®信号。这些信号包含一个匿名ID,该ID经过加密并不断更改以保护您的隐私。";
|
||||
"NSBluetoothPeripheralUsageDescription" = "COVIDSafe与附近运行同一应用程序的手机交换蓝牙®信号。这些信号包含一个匿名ID,该ID经过加密并不断更改以保护您的隐私。";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "";
|
||||
"NSLocationAlwaysUsageDescription" = "";
|
||||
"NSLocationWhenInUseUsageDescription" = "";
|
||||
"UILaunchStoryboardName" = "LaunchScreen_zh-Hans";
|
||||
|
|
|
@ -239,7 +239,7 @@
|
|||
/* OnBoarding Data Privacy */
|
||||
"data_privacy_headline" = "注册及隐私";
|
||||
"deaths" = "死亡人数";
|
||||
"dialog_error_uploading_message" = "个人信息上传时发生错误,请重试。";
|
||||
"dialog_error_uploading_message" = "";
|
||||
"dialog_error_uploading_negative" = "取消";
|
||||
"dialog_error_uploading_positive" = "重试";
|
||||
"dialog_uploading_message" = "您的COVIDSafe信息正在上传中。 \n\n请不要关闭该应用程序。";
|
||||
|
@ -317,6 +317,8 @@
|
|||
"jwt_heading" = "请重新注册";
|
||||
"jwt_success" = "";
|
||||
"loading_numbers" = "正在加载最新统计数字";
|
||||
"location_off" = "";
|
||||
"location_off_description" = "";
|
||||
/* Splash Screen */
|
||||
"migration_in_progress" = "COVIDSafe正在更新。 \n\n请保持开机状态直至更新完成。";
|
||||
"national_numbers" = "全国统计数字";
|
||||
|
@ -343,9 +345,8 @@
|
|||
"options_for_australia" = "澳大利亚选项";
|
||||
"permission_button" = "继续";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS" = "COVIDSafe需要启用蓝牙®才能运行。通过启用通知,您将在COVIDSafe未激活时收到提醒。 \n\n选择“继续”以启用:";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS_2" = "1. 蓝牙®\n2. 通知\n\nCOVIDSafe 不会发送配对请求。";
|
||||
"permission_content_iOS" = "";
|
||||
"permission_content_iOS_2" = "";
|
||||
/* OnBoarding Permission */
|
||||
"permission_headline" = "应用设置";
|
||||
"permission_success_button" = "完成";
|
||||
|
@ -412,10 +413,15 @@
|
|||
"update_available_dismiss_btn" = "稍后提醒我";
|
||||
"update_available_message_ios" = "我们一直在改进 COVIDSafe。请通过应用商店更新。";
|
||||
"update_available_title" = "更新可用!";
|
||||
"update_description" = "";
|
||||
"update_description_VO" = "";
|
||||
"update_heading" = "";
|
||||
"update_modal_button" = "继续";
|
||||
"upload_answer_no" = "否";
|
||||
/* Upload flow */
|
||||
"upload_answer_yes" = "是";
|
||||
"upload_consent_button" = "我同意";
|
||||
"upload_fail_heading" = "";
|
||||
"upload_failed" = "上传失败";
|
||||
"upload_finished_header" = "感谢您协助阻止COVID-19的传播!";
|
||||
"upload_finished_sub_header" = "您已成功将个人信息上传到COVIDSafe高度安全的储存系统。 \n\n根据记录,其他COVIDSafe用户与您如有密切接触,州或领地卫生官员将通知他们。您的身份对其他用户将保持匿名状态。";
|
||||
|
@ -501,7 +507,7 @@
|
|||
"NotificationsDisabled_VOLabel" = "通知已禁用。如果COVIDSafe未激活,您将不会收到通知。 \n更改通知设置";
|
||||
"NotificationsEnabled_VOLabel" = "通知已启用。如果COVIDSafe未激活,您将收到通知。 \n更改通知设置";
|
||||
"OS1b_TopParagraph_VOLabel" = "COVIDSafe需要使用蓝牙®信号以确定您靠近另一个COVIDSafe用户的时间。 \n\n您与其他COVIDSafe用户之间的每一次密切接触都被记录下来,以创建密切接触信息。该信息经过加密,且仅储存在您的手机中。 \n\n如果您使用COVIDSafe并对COVID-19测试呈阳性,则州或领地卫生官员将与您联系。经您同意,他们会协助您将密切接触者信息上传至高度安全的信息储存系统。\n\n如果您与测试结果呈阳性的其他COVIDSafe用户发生密切接触,则州或领地卫生官员也会与您联系。 \n\n如需更多信息,请参阅*帮助主题*页。";
|
||||
"permission_content_iOS_2_VOLabel" = "1. 蓝牙®\n2. 通知\n\nCOVIDSafe不会发送配对请求。";
|
||||
"permission_content_iOS_VOLabel" = "COVIDSafe需要启用蓝牙®才能运行。通过启用通知,您将在COVIDSafe未激活时收到提醒。 \n\n选择“继续”以启用:";
|
||||
"permission_content_iOS_2_VOLabel" = "";
|
||||
"permission_content_iOS_VOLabel" = "";
|
||||
"ShareCovidSafe_VOLabel" = "共享COVIDSafe。邀请他人助力。万众一心,其利断金。";
|
||||
"UploadData_VOLabel" = "是否有卫生官员联系过您?只有在您的病毒检测呈阳性时,才能上传个人信息。";
|
||||
|
|
|
@ -5,4 +5,7 @@
|
|||
"BluetoothUsageDesc" = "COVIDSafe 與附近正在執行同一應用程式的手機交換藍牙®訊號。這些訊號包括一條匿名 ID,該 ID 會被加密且會不斷更新以保護你的私隱。";
|
||||
"NSBluetoothAlwaysUsageDescription" = "COVIDSafe 與附近運行相同程式的電話交換藍牙® 信號。這些信號含有一個匿名ID,會加密並且不斷改變來保證你的私隱。";
|
||||
"NSBluetoothPeripheralUsageDescription" = "COVIDSafe 與附近運行相同程式的電話交換藍牙® 信號。這些信號含有一個匿名ID,會加密並且不斷改變來保證你的私隱。";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "";
|
||||
"NSLocationAlwaysUsageDescription" = "";
|
||||
"NSLocationWhenInUseUsageDescription" = "";
|
||||
"UILaunchStoryboardName" = "LaunchScreen_zh-Hant";
|
||||
|
|
|
@ -239,7 +239,7 @@
|
|||
/* OnBoarding Data Privacy */
|
||||
"data_privacy_headline" = "註册及私隱權";
|
||||
"deaths" = "死亡人數";
|
||||
"dialog_error_uploading_message" = "個人資料上傳時發生錯誤,請重試。";
|
||||
"dialog_error_uploading_message" = "";
|
||||
"dialog_error_uploading_negative" = "取消";
|
||||
"dialog_error_uploading_positive" = "重試";
|
||||
"dialog_uploading_message" = "正在上傳你的 COVIDSafe 資料。 \n\n請勿關閉應用程式。";
|
||||
|
@ -317,6 +317,8 @@
|
|||
"jwt_heading" = "請重新註冊。";
|
||||
"jwt_success" = "";
|
||||
"loading_numbers" = "正在載入最新統計數字";
|
||||
"location_off" = "";
|
||||
"location_off_description" = "";
|
||||
/* Splash Screen */
|
||||
"migration_in_progress" = "正在更新 COVIDSafe。\n\n請保持開機狀態直至更新完成為止。";
|
||||
"national_numbers" = "全國統計數字";
|
||||
|
@ -343,9 +345,8 @@
|
|||
"options_for_australia" = "澳洲適用選項";
|
||||
"permission_button" = "繼續";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS" = "COVIDSafe 需要開啓藍牙®才能執行。透過開啟「通知傳送」功能,你會在 COVIDSafe 仍未處於啟用狀態時收到提醒。\n\n請選擇「繼續」來開啟:";
|
||||
/* Figma page iOS Screens (05/06) */
|
||||
"permission_content_iOS_2" = "1. 藍牙®\n2. 通知\n\nCOVIDSafe 不會傳送配對請求。";
|
||||
"permission_content_iOS" = "";
|
||||
"permission_content_iOS_2" = "";
|
||||
/* OnBoarding Permission */
|
||||
"permission_headline" = "應用程式設定";
|
||||
"permission_success_button" = "完成";
|
||||
|
@ -412,10 +413,15 @@
|
|||
"update_available_dismiss_btn" = "請稍後提醒";
|
||||
"update_available_message_ios" = "我們一直都在改良 COVIDSafe。通過App Store更新。";
|
||||
"update_available_title" = "更新可用!";
|
||||
"update_description" = "";
|
||||
"update_description_VO" = "";
|
||||
"update_heading" = "";
|
||||
"update_modal_button" = "繼續";
|
||||
"upload_answer_no" = "否";
|
||||
/* Upload flow */
|
||||
"upload_answer_yes" = "是";
|
||||
"upload_consent_button" = "我同意";
|
||||
"upload_fail_heading" = "";
|
||||
"upload_failed" = "上傳失敗";
|
||||
"upload_finished_header" = "感謝你協助遏止 COVID-19 的擴散!";
|
||||
"upload_finished_sub_header" = "你已成功將個人資料上傳到 COVIDSafe 高度保密的儲存系統。\n\n若錄得有其他 COVIDSafe 用户曾密切接觸過你,各州或領地的衛生部官員將會通知他們。你的身份仍舊予以保密。";
|
||||
|
@ -501,7 +507,7 @@
|
|||
"NotificationsDisabled_VOLabel" = "「通知傳送」功能已被闗閉。如果 COVIDSafe 尚未處於啟用狀態,你將不會收到通知。更改「通知傳送」設定";
|
||||
"NotificationsEnabled_VOLabel" = "「通知傳送」功能已被開啓。如果 COVIDSafe 尚未處於啟用狀態,你將會收到通知。更改「通知傳送」設定";
|
||||
"OS1b_TopParagraph_VOLabel" = "藍牙®訊號用於確定你在何時接近另一名 COVIDSafe 用戶。 \n你與其他 COVIDSafe 用戶的每次密切接觸都會被記錄下來,以創建密切接觸者資料。該資料會被加密,並僅儲存在你的手機内。 \n如果你是一名 COVIDSafe 用戶,而又被驗出 COVID-19 陽性,則州或領地衛生官員將會與你聯絡。他們將會協助你自願把你自己的密切接觸者資料上傳到高度保密的資料儲存系統。\n如果你曾密切接觸過另一名化驗結果呈陽性的 COVIDSafe 用戶,則州或領地衛生官員也可能會聯絡你。 \n欲知詳情,請參閱「求助主題」頁。";
|
||||
"permission_content_iOS_2_VOLabel" = "1. 藍牙®\n2. 通知\n\nCOVIDSafe 不會傳送配對請求。";
|
||||
"permission_content_iOS_VOLabel" = "需開啓藍牙®才能執行 COVIDSafe 。透過開啟「通知傳送」功能,你會在 COVIDSafe 尚未處於啟用狀態時收到提醒。\n\n請選擇「繼續」來開啟:";
|
||||
"permission_content_iOS_2_VOLabel" = "";
|
||||
"permission_content_iOS_VOLabel" = "";
|
||||
"ShareCovidSafe_VOLabel" = "分享使用 COVIDSafe,邀請他人參與防疫。團結便是力量!";
|
||||
"UploadData_VOLabel" = "曾有衛官員聯絡過你嗎?只有在你被驗出病毒陽性時,才能上傳個人資料。";
|
||||
|
|
11
README.md
11
README.md
|
@ -1,3 +1,14 @@
|
|||
# The new Herald Protocol is being integrated into COVIDSafe
|
||||
|
||||
Even though we have made a range of significant improvements to COVIDSafe, we continue to look for ways we can improve the app further. Herald, a Bluetooth communication and range finding protocol that supports contact tracing, is one such improvement.
|
||||
|
||||
The Herald Protocol employs several techniques to improve Bluetooth communication across a wide range of mobile devices. This provides contact tracing apps with regular and accurate proximity information that helps make them highly effective, especially in background on iOS devices.
|
||||
|
||||
Herald is a VMware-originated open source project. It is part of VMware’s ongoing contribution towards the Linux Foundation Public Health initiative. The initiative aims to use open source technologies to help public health authorities across the world combat COVID-19.
|
||||
|
||||
Find out more about Herald and COVIDSafe: [https://www.dta.gov.au/news/covidsafe-captures-close-contacts-new-herald-protocol](https://www.dta.gov.au/news/covidsafe-captures-close-contacts-new-herald-protocol)
|
||||
|
||||
|
||||
# COVIDSafe app
|
||||
|
||||
Thank you for viewing the GitHub repository for the COVIDSafe app by the Australian Government.
|
||||
|
|
Loading…
Reference in a new issue