diff --git a/.github/workflows/deploy_to_cocoapods.yml b/.github/workflows/deploy_to_cocoapods.yml
new file mode 100644
index 00000000..c8edb636
--- /dev/null
+++ b/.github/workflows/deploy_to_cocoapods.yml
@@ -0,0 +1,29 @@
+name: deploy_to_cocoapods
+
+on:
+ push:
+ tags:
+ - '*'
+
+jobs:
+ build:
+
+ runs-on: macOS-latest
+
+ steps:
+ - uses: actions/checkout@v1
+
+ - name: Switch to Xcode 11.5
+ run: sudo xcode-select --switch /Applications/Xcode_11.5.app
+
+ - name: Install Cocoapods
+ run: gem install cocoapods
+
+ - name: Deploy to Cocoapods
+ run: |
+ set -eo pipefail
+ export LIB_VERSION=$(git describe --tags `git rev-list --tags --max-count=1`)
+ pod lib lint --allow-warnings
+ pod trunk push DP3TSDK.podspec --allow-warnings
+ env:
+ COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1eb17ad8..34136b27 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog for DP3T-SDK iOS
+## Version 1.1.0 (17.07.2020)
+- adds background refresh task to improve background time
+- retrieves keys in background on iOS > 13.6
+- expose data if HTTP Code is not expected
+- handle case if EN Framework is not available (iOS 14 beta)
+- defer schedule background task until EN is authorized
+- retrys activation and enabling of ENManager if failed on willEnterForeground
+
## Version 1.0.2 (03.07.2020)
- defers sync until ENManager is fully initialized
- fixes in background task handling
diff --git a/DP3TSDK.podspec b/DP3TSDK.podspec
index 87efcfc2..dc9b15d3 100644
--- a/DP3TSDK.podspec
+++ b/DP3TSDK.podspec
@@ -2,7 +2,7 @@
Pod::Spec.new do |spec|
spec.name = "DP3TSDK"
- spec.version = "1.0.2"
+ spec.version = ENV['LIB_VERSION'] || '1.1.0'
spec.summary = "Open protocol for COVID-19 proximity tracing using Bluetooth Low Energy on mobile devices"
spec.description = <<-DESC
diff --git a/README.md b/README.md
index 7d595d17..c071870c 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ DP-3T is a free-standing effort started at EPFL and ETHZ that produced this prot
## Introduction
-This is the implementation of the DP-3T protocol using the [Exposure Notification](https://developer.apple.com/documentation/exposurenotification) Framework of Apple/Google. Only approved government public health authorities can access the APIs. Therefore, using this SDK will result in an API error unless you were granted the `com.apple.developer.exposure-notification` entitlement by apple. Therefore the minimum deployment target is iOS 13.5.
+This is the implementation of the DP-3T protocol using the [Exposure Notification](https://developer.apple.com/documentation/exposurenotification) Framework of Apple/Google. Only approved government public health authorities can access the APIs. Therefore, using this SDK will result in an API error unless you were granted the `com.apple.developer.exposure-notification` entitlement by Apple. The ExposureNotification.framework is available starting with iOS 13.5.
Our prestandard solution that is not using the Apple/Google framework can be found under the [tag prestandard](https://github.com/DP-3T/dp3t-sdk-ios/tree/prestandard).
@@ -25,7 +25,7 @@ Our prestandard solution that is not using the Apple/Google framework can be fou
The full set of documents for DP3T is at https://github.com/DP-3T/documents. Please refer to the technical documents and whitepapers for a description of the implementation.
## Calibration App
-Included in this repository is a Calibration App that can run, debug and test the SDK directly without implementing it in a new app first. It collects additional data and stores it locally into a database to allow for tests with phones from different vendors. Various parameters of the SDK are exposed and can be changed at runtime. Additionally it provides an overview of how to use the SDK.
+Included in this repository is a Calibration App that can run, debug and test the SDK directly without implementing it in a new app first. Various parameters of the SDK are exposed and can be changed at runtime. Additionally it provides an overview of how to use the SDK.
@@ -44,12 +44,12 @@ init | Initializes the SDK and configures it | `initialize(applicationDescriptor
### Methods
Name | Description | Function Name
---- | ----------- | -------------
-startTracing | Starts Bluetooth tracing | `func startTracing(completionHandler: )throws`
-stopTracing | Stops Bluetooth tracing | `func stopTracing(completionHandler:)`
+startTracing | Starts EN tracing | `func startTracing(completionHandler: )throws`
+stopTracing | Stops EN tracing | `func stopTracing(completionHandler:)`
sync | Pro-actively triggers sync with backend to refresh exposed list | `func sync(callback:)`
status | Returns a TracingState-Object describing the current state. This contains:
- `numberOfHandshakes` : `Int`
- `trackingState` : `TrackingState`
- `lastSync` : `Date`
- `infectionStatus`:`InfectionStatus`
- `backgroundRefreshState`:`UIBackgroundRefreshStatus ` | `func status(callback:)`
iWasExposed | This method must be called upon positive test. | `func iWasExposed(onset:authentication:isFakeRequest:callback:)`
-reset | Removes all SDK related data (key and database) and de-initializes SDK | `func reset() throws`
+reset | Removes all SDK related data | `func reset() throws`
## Installation
@@ -74,7 +74,7 @@ DP3T-SDK is available through [Cocoapods](https://cocoapods.org/)
```ruby
- pod 'DP3TSDK', => '1.0.2'
+ pod 'DP3TSDK', => '1.1.0'
```
@@ -105,20 +105,6 @@ To start and stop tracing use
try DP3TTracing.startTracing()
DP3TTracing.stopTracing()
```
-Make sure that the app includes in the `Info.plist` the bluetooth keys `NSBluetoothAlwaysUsageDescription` and `NSBluetoothPeripheralUsageDescription` and that the user has granted the app permission to use the Bluetooth periferals. Also the app as to support `BackgroundMode` capability for `bluetooth-central` and `bluetooth-peripheral`.
-
-`Info.plist` sample:
-```swift
-UIBackgroundModes
-
- bluetooth-central
- bluetooth-peripheral
-
-NSBluetoothAlwaysUsageDescription
- User facing text justifying bluetooth usage
-NSBluetoothPeripheralUsageDescription
- User facing text justifying bluetooth usage
-```
### Checking the current tracing status
```swift
@@ -154,22 +140,24 @@ DP3TTracing.sync() { result in
#### Background Tasks
-The SDK supports iOS 13 Background tasks. To enable them the app has to support the `Background process` capability and include `org.dpppt.exposure-notification` in the `BGTaskSchedulerPermittedIdentifiers` `Info.plist` property.
+The SDK supports iOS 13 background tasks. It uses the provided `exposure-notification` background processing task as well as the `BGAppRefreshTask`. To enable them the app has to support the `Background process` and `Background fetch` capabilities and include `org.dpppt.exposure-notification` and `org.dpppt.refresh` in the `BGTaskSchedulerPermittedIdentifiers` `Info.plist` property.
`Info.plist` sample:
```swift
BGTaskSchedulerPermittedIdentifiers
- org.dpppt.exposure-notification
+ org.dpppt.exposure-notification
+ org.dpppt.refresh
UIBackgroundModes
-
- processing
+
+ processing
+ fetch
```
-If a DP3TBackgroundHandler was passed to the SDK on initialisation it will be called on each background task execution by the SDK.
+If a `DP3TBackgroundHandler` was passed to the SDK on initialisation it will be called on each background task execution by the SDK.
## License
diff --git a/SampleApp/DP3TSampleApp/Info.plist b/SampleApp/DP3TSampleApp/Info.plist
index 38f10312..ec496701 100644
--- a/SampleApp/DP3TSampleApp/Info.plist
+++ b/SampleApp/DP3TSampleApp/Info.plist
@@ -5,6 +5,7 @@
BGTaskSchedulerPermittedIdentifiers
org.dpppt.exposure-notification
+ org.dpppt.refresh
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
diff --git a/Sources/DP3TSDK/Background/DP3TBackgroundTaskManager.swift b/Sources/DP3TSDK/Background/DP3TBackgroundTaskManager.swift
index a3ff9920..381c9898 100644
--- a/Sources/DP3TSDK/Background/DP3TBackgroundTaskManager.swift
+++ b/Sources/DP3TSDK/Background/DP3TBackgroundTaskManager.swift
@@ -13,7 +13,8 @@ import Foundation
import UIKit.UIApplication
class DP3TBackgroundTaskManager {
- static let taskIdentifier: String = "org.dpppt.exposure-notification"
+ static let exposureNotificationTaskIdentifier: String = "org.dpppt.exposure-notification"
+ static let refreshTaskIdentifier: String = "org.dpppt.refresh"
/// Background task registration should only happen once per run
/// If the SDK gets destroyed and initialized again this would cause a crash
@@ -51,17 +52,23 @@ class DP3TBackgroundTaskManager {
guard !Self.didRegisterBackgroundTask else { return }
Self.didRegisterBackgroundTask = true
- BGTaskScheduler.shared.register(forTaskWithIdentifier: DP3TBackgroundTaskManager.taskIdentifier, using: .main) { task in
- self.handleBackgroundTask(task)
+ BGTaskScheduler.shared.register(forTaskWithIdentifier: DP3TBackgroundTaskManager.exposureNotificationTaskIdentifier, using: .main) { task in
+ self.handleExposureNotificationBackgroundTask(task)
+ }
+
+ BGTaskScheduler.shared.register(forTaskWithIdentifier: DP3TBackgroundTaskManager.refreshTaskIdentifier, using: .main) { task in
+ // Downcast the parameter to an app refresh task as this identifier is used for a refresh request.
+ self.handleRefreshTask(task as! BGAppRefreshTask)
}
}
@objc func appDidEnterBackground(){
- scheduleBackgroundTask()
+ scheduleBackgroundTasks()
}
- private func handleBackgroundTask(_ task: BGTask) {
+ private func handleExposureNotificationBackgroundTask(_ task: BGTask) {
logger.trace()
+ scheduleBackgroundTasks()
let queue = OperationQueue()
@@ -72,7 +79,7 @@ class DP3TBackgroundTaskManager {
completionGroup.enter()
handlerOperation.completionBlock = { [weak self] in
- self?.logger.log("handlerOperation finished")
+ self?.logger.log("Exposure notification handlerOperation finished")
completionGroup.leave()
}
@@ -83,40 +90,91 @@ class DP3TBackgroundTaskManager {
completionGroup.enter()
syncOperation.completionBlock = { [weak self] in
- self?.logger.log("syncOperation finished")
+ self?.logger.log("SyncOperation finished")
completionGroup.leave()
}
queue.addOperation(syncOperation)
task.expirationHandler = { [weak self] in
- self?.logger.error("DP3TBackgroundTaskManager expiration handler called")
+ self?.logger.error("Exposure notification task expiration handler called")
queue.cancelAllOperations()
}
completionGroup.notify(queue: .main) { [weak self] in
- self?.logger.log("DP3TBackgroundTaskManager task completed")
+ self?.logger.log("Exposure notification task completed")
let success = !queue.operations.map { $0.isCancelled }.contains(true)
task.setTaskCompleted(success: success)
}
+ }
+
+ private func handleRefreshTask(_ task: BGTask) {
+ logger.trace()
+ scheduleBackgroundTasks()
+
+ let queue = OperationQueue()
+ let completionGroup = DispatchGroup()
+
+ if let handler = handler {
+ let handlerOperation = HandlerOperation(handler: handler)
+
+ completionGroup.enter()
+ handlerOperation.completionBlock = { [weak self] in
+ self?.logger.log("Refresh handlerOperation finished")
+ completionGroup.leave()
+ }
+
+ queue.addOperation(handlerOperation)
+ }
- scheduleBackgroundTask()
+ let outstandingPublishOperation = OutstandingPublishOperation(keyProvider: keyProvider,
+ serviceClient: serviceClient,
+ runningInBackground: true)
+ completionGroup.enter()
+ outstandingPublishOperation.completionBlock = {
+ completionGroup.leave()
+ }
+ queue.addOperation(outstandingPublishOperation)
+
+ task.expirationHandler = { [weak self] in
+ self?.logger.error("Refresh task expiration handler called")
+ queue.cancelAllOperations()
+ }
+
+ completionGroup.notify(queue: .main) { [weak self] in
+ self?.logger.log("Refresh task completed")
+
+ let success = !queue.operations.map { $0.isCancelled }.contains(true)
+ task.setTaskCompleted(success: success)
+ }
}
- private func scheduleBackgroundTask() {
+ private func scheduleBackgroundTasks() {
logger.trace()
+
+ // Schedule next app refresh task 12h in the future
+ let refreshRequest = BGAppRefreshTaskRequest(identifier: DP3TBackgroundTaskManager.refreshTaskIdentifier)
+ refreshRequest.earliestBeginDate = Date(timeIntervalSinceNow: 12 * 60 * 60)
+
+ do {
+ try BGTaskScheduler.shared.submit(refreshRequest)
+ } catch {
+ logger.error("Scheduling refresh task failed error: %{public}@", error.localizedDescription)
+ }
+
+ // Only schedule exposure notification task after EN is authorized
guard tracer.isAuthorized else {
- logger.log("skipping schedule because ENManager is not authorized")
+ logger.log("Skipping scheduling of exposure notification task because ENManager is not authorized")
return
}
- let taskRequest = BGProcessingTaskRequest(identifier: DP3TBackgroundTaskManager.taskIdentifier)
+ let taskRequest = BGProcessingTaskRequest(identifier: DP3TBackgroundTaskManager.exposureNotificationTaskIdentifier)
taskRequest.requiresNetworkConnectivity = true
do {
handler?.didScheduleBackgrounTask()
try BGTaskScheduler.shared.submit(taskRequest)
} catch {
- logger.error("background task schedule failed error: %{public}@", error.localizedDescription)
+ logger.error("Exposure notification task schedule failed error: %{public}@", error.localizedDescription)
}
}
}
diff --git a/Sources/DP3TSDK/Background/OutstandingPublishOperation.swift b/Sources/DP3TSDK/Background/OutstandingPublishOperation.swift
index a0cef12e..9f6f38f7 100644
--- a/Sources/DP3TSDK/Background/OutstandingPublishOperation.swift
+++ b/Sources/DP3TSDK/Background/OutstandingPublishOperation.swift
@@ -56,10 +56,15 @@ class OutstandingPublishOperation: Operation {
continue
}
- if runningInBackground {
- // skip publish if we are not in foreground since apple does not allow calles to EN.getDiagnosisKeys in background
- logger.log("skipping outstanding key %{public}@ because we are not in foreground", op.debugDescription)
- continue
+ if #available(iOS 13.6, *) {
+ // this was fixed by apple with iOS 13.6 beta 4
+ // (there is unfortunally no way to negate #available checks)
+ } else {
+ if runningInBackground {
+ // skip publish if we are not in foreground since apple does not allow calles to EN.getDiagnosisKeys in background
+ logger.log("skipping outstanding key %{public}@ because we are not in foreground", op.debugDescription)
+ continue
+ }
}
logger.log("handling outstanding Publish %@", op.debugDescription)
diff --git a/Sources/DP3TSDK/DP3TTracing.swift b/Sources/DP3TSDK/DP3TTracing.swift
index e78ddb3d..7574d44a 100644
--- a/Sources/DP3TSDK/DP3TTracing.swift
+++ b/Sources/DP3TSDK/DP3TTracing.swift
@@ -28,7 +28,7 @@ private var instance: DP3TSDK!
/// DP3TTracing
public enum DP3TTracing {
/// The current version of the SDK
- public static let frameworkVersion: String = "1.0.2"
+ public static let frameworkVersion: String = "1.1.0"
/// sets global parameter values which are used throughout the sdk
public static var parameters: DP3TParameters {
diff --git a/Sources/DP3TSDK/Matching/ExposureNotificationMatcher.swift b/Sources/DP3TSDK/Matching/ExposureNotificationMatcher.swift
index f2541019..fe4923bf 100644
--- a/Sources/DP3TSDK/Matching/ExposureNotificationMatcher.swift
+++ b/Sources/DP3TSDK/Matching/ExposureNotificationMatcher.swift
@@ -56,7 +56,7 @@ class ExposureNotificationMatcher: Matcher {
let semaphore = DispatchSemaphore(value: 0)
var exposureSummary: ENExposureDetectionSummary?
- var exposureDetectionError: Error?
+ var exposureDetectionError: Error? = DP3TTracingError.cancelled
logger.log("calling detectExposures for day %{public}@ and description: %{public}@", keyDate.description, configuration.stringVal)
manager.detectExposures(configuration: configuration, diagnosisKeyURLs: urls) { summary, error in
@@ -64,7 +64,16 @@ class ExposureNotificationMatcher: Matcher {
exposureDetectionError = error
semaphore.signal()
}
- semaphore.wait()
+
+ // Wait for 3min and abort if detectExposures did not return in time
+ if semaphore.wait(timeout: .now() + 180) == .timedOut {
+ // This should never be the case but it protects us from errors
+ // in ExposureNotifications.frameworks which cause the completion
+ // handler to never get called.
+ // If ENManager would return after 3min, the app gets kill before
+ // that because we are only allowed to run for 2.5min in background
+ logger.error("ENManager.detectExposures() failed to return in time")
+ }
if let error = exposureDetectionError {
logger.error("ENManager.detectExposures failed error: %{public}@", error.localizedDescription)
diff --git a/Sources/DP3TSDK/Networking/KnownCasesSynchronizer.swift b/Sources/DP3TSDK/Networking/KnownCasesSynchronizer.swift
index ed970326..9620b1e1 100644
--- a/Sources/DP3TSDK/Networking/KnownCasesSynchronizer.swift
+++ b/Sources/DP3TSDK/Networking/KnownCasesSynchronizer.swift
@@ -40,7 +40,7 @@ class KnownCasesSynchronizer {
private var callbacks: [Callback] = []
- private var backgroundTask: UIBackgroundTaskIdentifier?
+ private var backgroundTask: UIBackgroundTaskIdentifier = .invalid
private var dataTasks: [URLSessionDataTask] = []
@@ -88,20 +88,21 @@ class KnownCasesSynchronizer {
self.callbacks.append(callback)
// If we already have a background task we need to cancel it
- if let backgroundTask = self.backgroundTask, backgroundTask != .invalid {
- UIApplication.shared.endBackgroundTask(backgroundTask)
+ if self.backgroundTask != .invalid {
+ UIApplication.shared.endBackgroundTask(self.backgroundTask)
self.backgroundTask = .invalid
}
self.backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "org.dpppt.sync") { [weak self] in
guard let self = self else { return }
- self.cancelSync()
+ UIApplication.shared.endBackgroundTask(self.backgroundTask)
+ self.backgroundTask = .invalid
}
self.internalSync(now: now) { [weak self] result in
guard let self = self else { return }
self.queue.async {
- UIApplication.shared.endBackgroundTask(self.backgroundTask!)
+ UIApplication.shared.endBackgroundTask(self.backgroundTask)
self.backgroundTask = .invalid
self.callbacks.forEach { $0(result) }
self.callbacks.removeAll()
@@ -111,7 +112,7 @@ class KnownCasesSynchronizer {
}
func cancelSync() {
- queue.sync { [weak self] in
+ queue.async { [weak self] in
guard let self = self else { return }
self.isCancelled = true
diff --git a/Sources/DP3TSDK/Tracing/ExposureNotificationTracer.swift b/Sources/DP3TSDK/Tracing/ExposureNotificationTracer.swift
index c1fda0e4..2138e195 100644
--- a/Sources/DP3TSDK/Tracing/ExposureNotificationTracer.swift
+++ b/Sources/DP3TSDK/Tracing/ExposureNotificationTracer.swift
@@ -28,6 +28,10 @@ class ExposureNotificationTracer: Tracer {
private let managerClass: ENManager.Type
+ private var isActivated: Bool = false
+
+ private var deferredEnable: Bool?
+
private(set) var state: TrackingState {
didSet {
guard oldValue != state else { return }
@@ -42,14 +46,29 @@ class ExposureNotificationTracer: Tracer {
state = .initialization
+ activateManager()
+
+ NotificationCenter.default.addObserver(self,
+ selector: #selector(willEnterForeground),
+ name: UIApplication.willEnterForegroundNotification,
+ object: nil)
+ }
+
+ func activateManager(){
logger.log("calling ENMananger.activate")
manager.activate { [weak self] error in
guard let self = self else { return }
self.queue.async {
if let error = error {
self.logger.error("ENMananger.activate failed error: %{public}@", error.localizedDescription)
+ self.state = .inactive(error: .exposureNotificationError(error: error))
} else {
+ self.isActivated = true
self.initializeObservers()
+
+ if let deferredEnable = self.deferredEnable {
+ self.setEnabled(deferredEnable, completionHandler: nil)
+ }
}
self.logger.log("notify callbacks after initialisation (count: %d)", self.initializationCallbacks.count)
self.initializationCallbacks.forEach {
@@ -58,10 +77,6 @@ class ExposureNotificationTracer: Tracer {
self.initializationCallbacks.removeAll()
}
}
- NotificationCenter.default.addObserver(self,
- selector: #selector(willEnterForeground),
- name: UIApplication.willEnterForegroundNotification,
- object: nil)
}
func addInitialisationCallback(callback: @escaping ()-> Void ){
@@ -83,7 +98,15 @@ class ExposureNotificationTracer: Tracer {
var isAuthorized: Bool { ENManager.authorizationStatus == .authorized }
@objc func willEnterForeground(){
- updateState()
+ self.queue.async {
+ if !self.isActivated {
+ self.activateManager()
+ } else if let deferredEnable = self.deferredEnable {
+ self.setEnabled(deferredEnable, completionHandler: nil)
+ } else {
+ self.updateState()
+ }
+ }
}
func initializeObservers() {
@@ -99,21 +122,41 @@ class ExposureNotificationTracer: Tracer {
}
func updateState() {
- state = .init(state: manager.exposureNotificationStatus,
- authorizationStatus: managerClass.authorizationStatus,
- enabled: manager.exposureNotificationEnabled)
+ guard self.isActivated else { return }
+
+ self.state = .init(state: self.manager.exposureNotificationStatus,
+ authorizationStatus: self.managerClass.authorizationStatus,
+ enabled: self.manager.exposureNotificationEnabled)
}
func setEnabled(_ enabled: Bool, completionHandler: ((Error?) -> Void)?) {
logger.log("calling ENMananger.setExposureNotificationEnabled %{public}@", enabled ? "true" : "false")
+ guard self.isActivated else {
+ logger.log("could not enable since manager is not activated")
+ self.deferredEnable = enabled
+
+ // use stored error if available
+ if case let TrackingState.inactive(error: error) = state {
+ completionHandler?(error)
+ } else {
+ completionHandler?(DP3TTracingError.permissonError)
+ }
+ return
+ }
+
+ deferredEnable = nil
+
manager.setExposureNotificationEnabled(enabled) { [weak self] error in
guard let self = self else { return }
if let error = error {
self.logger.error("ENMananger.setExposureNotificationEnabled failed error: %{public}@", error.localizedDescription)
+ self.deferredEnable = enabled
self.state = .inactive(error: .exposureNotificationError(error: error))
+ } else {
+ self.deferredEnable = nil
+ self.updateState()
}
- self.updateState()
completionHandler?(error)
}
}
@@ -121,11 +164,6 @@ class ExposureNotificationTracer: Tracer {
extension TrackingState {
init(state: ENStatus, authorizationStatus: ENAuthorizationStatus, enabled: Bool) {
- if authorizationStatus == .unknown {
- self = .stopped
- return
- }
-
guard authorizationStatus == .authorized else {
self = .inactive(error: .permissonError)
return
diff --git a/Tests/DP3TSDKTests/DP3TSDKTests.swift b/Tests/DP3TSDKTests/DP3TSDKTests.swift
index b230fa61..ea0d7f73 100644
--- a/Tests/DP3TSDKTests/DP3TSDKTests.swift
+++ b/Tests/DP3TSDKTests/DP3TSDKTests.swift
@@ -101,11 +101,12 @@ class DP3TSDKTests: XCTestCase {
}
func testCallEnable(){
+ manager.completeActivation()
let exp = expectation(description: "enable")
try! sdk.startTracing { (err) in
exp.fulfill()
}
- wait(for: [exp], timeout: 0.1)
+ wait(for: [exp], timeout: 1.0)
XCTAssertEqual(tracer.state, TrackingState.active)
}
@@ -201,4 +202,122 @@ class DP3TSDKTests: XCTestCase {
wait(for: [exp], timeout: 1.0)
XCTAssertEqual(service.requests.count, 10)
}
+
+ struct MockError: Error, Equatable {
+ let message: String
+ }
+
+ func testActivationAfterFailure() {
+ MockENManager.authStatus = .unknown
+ let message = "mockError"
+ manager.completeActivation(error: MockError(message: message))
+ sleep(1)
+ let expStatus = expectation(description: "status")
+ sdk.status { (result) in
+ switch result {
+ case let .success(state):
+ switch state.trackingState {
+ case let .inactive(error: error):
+ switch error {
+ case let .exposureNotificationError(error: enError):
+ guard let mockError = enError as? MockError else {
+ XCTFail()
+ return
+ }
+ XCTAssertEqual(mockError.message, message)
+ default:
+ XCTFail()
+ }
+ default:
+ XCTFail()
+ }
+ case .failure(_):
+ XCTFail()
+ }
+ expStatus.fulfill()
+ }
+ wait(for: [expStatus], timeout: 0.1)
+
+
+ // app comes again in foreground
+ tracer.willEnterForeground()
+ sleep(1)
+
+ MockENManager.authStatus = .authorized
+ manager.completeActivation()
+ sleep(1)
+
+ let expStatusAfter = expectation(description: "statusafter")
+ sdk.status { (result) in
+ switch result {
+ case let .success(state):
+ switch state.trackingState {
+ case .stopped:
+ break;
+ default:
+ XCTFail()
+ }
+ case .failure(_):
+ XCTFail()
+ }
+ expStatusAfter.fulfill()
+ }
+ wait(for: [expStatusAfter], timeout: 0.1)
+ }
+
+ func testEnableAfterEnableFailure(){
+ manager.enableError = MockError(message: "message")
+ manager.completeActivation()
+ let exp = expectation(description: "enable")
+ try! sdk.startTracing { (err) in
+ XCTAssert(err != nil)
+ XCTAssertEqual((err! as! MockError).message, "message")
+ exp.fulfill()
+ }
+ wait(for: [exp], timeout: 1.0)
+ XCTAssertEqual(tracer.state, TrackingState.inactive(error: .exposureNotificationError(error: manager.enableError!)))
+
+
+ manager.enableError = nil
+
+ // app comes again in foreground
+ tracer.willEnterForeground()
+
+ sleep(1)
+
+ XCTAssertEqual(tracer.state, TrackingState.active)
+ }
+
+ func testEnableAfterActivationFailure(){
+ MockENManager.authStatus = .unknown
+ let error = MockError(message: "mockError")
+
+ manager.completeActivation(error: error)
+ sleep(1)
+ let exp = expectation(description: "enable")
+ try! sdk.startTracing { (err) in
+ XCTAssert(err != nil)
+ switch (err! as! DP3TTracingError) {
+ case let .exposureNotificationError(error: enError):
+ XCTAssertEqual(enError as! MockError, error)
+ default:
+ XCTFail()
+ }
+ exp.fulfill()
+ }
+ wait(for: [exp], timeout: 1.0)
+ XCTAssertEqual(tracer.state, TrackingState.inactive(error: .exposureNotificationError(error: error)))
+
+
+ // app comes again in foreground
+ tracer.willEnterForeground()
+
+ sleep(1)
+
+ manager.completeActivation()
+
+ sleep(1)
+
+ XCTAssertEqual(tracer.state, TrackingState.active)
+ }
}
diff --git a/Tests/DP3TSDKTests/Mocks/MockENManager.swift b/Tests/DP3TSDKTests/Mocks/MockENManager.swift
index 0696aca2..5f81e44b 100644
--- a/Tests/DP3TSDKTests/Mocks/MockENManager.swift
+++ b/Tests/DP3TSDKTests/Mocks/MockENManager.swift
@@ -41,6 +41,8 @@ class MockENManager: ENManager {
var summary = MockSummary()
+ var enableError: Error?
+
override func detectExposures(configuration _: ENExposureConfiguration, diagnosisKeyURLs: [URL], completionHandler: @escaping ENDetectExposuresHandler) -> Progress {
detectExposuresWasCalled = true
completionHandler(summary, nil)
@@ -56,14 +58,18 @@ class MockENManager: ENManager {
}
override func setExposureNotificationEnabled(_ enabled: Bool, completionHandler: @escaping ENErrorHandler) {
- self.isEnabled = enabled
- self.status = .active
- Self.authStatus = .authorized
- completionHandler(nil)
+ if let error = enableError {
+ completionHandler(error)
+ } else {
+ self.isEnabled = enabled
+ self.status = .active
+ Self.authStatus = .authorized
+ completionHandler(nil)
+ }
}
- func completeActivation(){
- activateCallbacks.forEach{ $0(nil)}
+ func completeActivation(error: Error? = nil){
+ activateCallbacks.forEach{ $0(error)}
activateCallbacks.removeAll()
}