Skip to content

Commit

Permalink
Merge pull request #180 from DP-3T/develop
Browse files Browse the repository at this point in the history
Develop -> Master
  • Loading branch information
stmitt authored Jul 3, 2020
2 parents b85e9cb + c6ab7f5 commit 69308ea
Show file tree
Hide file tree
Showing 17 changed files with 471 additions and 109 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog for DP3T-SDK iOS

## Version 1.0.2 (03.07.2020)
- defers sync until ENManager is fully initialized
- fixes in background task handling
- fix in storing of lastSync Date

## Version 1.0.1 (22.06.2020)
- Make timeshift detection independent from locale / region settings
- Update last sync timestamps of individual days that were successful even if some others failed
Expand Down
2 changes: 1 addition & 1 deletion DP3TSDK.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Pod::Spec.new do |spec|

spec.name = "DP3TSDK"
spec.version = "1.0.1"
spec.version = "1.0.2"
spec.summary = "Open protocol for COVID-19 proximity tracing using Bluetooth Low Energy on mobile devices"

spec.description = <<-DESC
Expand Down
20 changes: 10 additions & 10 deletions EXPOSURE_NOTIFICATION_API_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,27 @@ This document outlines the interaction of the SDK with the [Exposure Notificatio

## Enabling Exposure Notifications

To enable Exposure Notifications for our app we need to call ENManager.setExposureNotificationEnabled(true:completionHandler:). This will trigger a system popup asking the user to either enable Exposure Notifications on this device or (if another app is active) to switch to our app as active Exposure Notifications app. After the user gave or denied consent the completionHandler will be called.
To enable Exposure Notifications for our app we need to call [ENManager.setExposureNotificationEnabled(true:completionHandler:)](https://developer.apple.com/documentation/exposurenotification/enmanager/3583729-setexposurenotificationenabled). This will trigger a system popup asking the user to either enable Exposure Notifications on this device or (if another app is active) to switch to our app as active Exposure Notifications app. After the user gave or denied consent the completionHandler will be called.

## Disabling Exposure Notifications

To disable Exposure Notifications for our app we need to call ENManager.setExposureNotificationEnabled(false:completionHandler:).
To disable Exposure Notifications for our app we need to call [ENManager.setExposureNotificationEnabled(false:completionHandler:)](https://developer.apple.com/documentation/exposurenotification/enmanager/3583729-setexposurenotificationenabled).

## Exporting Temporary Exposure Keys

To retrieve the Temporary Exposure Keys (TEKs) we need to call ENManager.getDiagnosisKeys(completionHandler:). This will trigger a system popup asking the user if he wants to share the TEKs of the last 14 days with the app. If the user agrees to share the keys with the app the completion handler will get called with a maximum of 14 TEKs.
To retrieve the Temporary Exposure Keys (TEKs) we need to call [ENManager.getDiagnosisKeys(completionHandler:)](https://developer.apple.com/documentation/exposurenotification/enmanager/3583725-getdiagnosiskeys). This will trigger a system popup asking the user whether he wants to share the TEKs of the last 14 days with the app. If the user agrees to share the keys with the app the completion handler will get called with a maximum of 14 TEKs.

The TEK of the current day is never returned by ENManager.getDiagnosisKeys, but only the keys of the previous 13 days. After the user agreed to share the keys we can call ENManager.getDiagnosisKeys again on the following day and will then receive the TEK of the day the user agreed to share the keys as well. For this to work, the user has to open the App and give consent and we enable Exposure Notifications call ENManager.getDiagnosisKeys and disable it again afterwards.
The TEK of the current day is currently not returned by [getDiagnosisKeys(completionHandler:)](https://developer.apple.com/documentation/exposurenotification/enmanager/3583725-getdiagnosiskeys), but only the keys of the previous 13 days. After the user agreed to share the keys we call [getDiagnosisKeys(completionHandler:)](https://developer.apple.com/documentation/exposurenotification/enmanager/3583725-getdiagnosiskeys) again on the following day and will then receive the TEK of last day. For this to work, the user has to open the app, after which we will temporarily enable EN, then call [getDiagnosisKeys(completionHandler:)](https://developer.apple.com/documentation/exposurenotification/enmanager/3583725-getdiagnosiskeys) (which triggers a system popup once once) and disable EN again.

## Detecting Exposure

For a contact to be counted as a possible exposure it must be longer than a certain number of minutes on a certain day. The current implementation of the EN-framework does not expose this information. Our way to overcome this limitation is to pass the published keys for each day individually to the framework.
For a contact to be counted as a possible exposure it must be longer than a certain number of minutes on a certain day. The current implementation of the EN-framework does not expose this information. Our way to overcome this limitation is to pass the published keys to the framework grouped by day.

To check for exposure on a given day (we check the past 10 days) we need to call ENManager.detectExposures(configuration:diagnosisKeyURLs:completionHandler:). This method has three parameters:
To check for exposure on a given day (we check the past 10 days) we need to call [ENManager.detectExposures(configuration:diagnosisKeyURLs:completionHandler:)](https://developer.apple.com/documentation/exposurenotification/enmanager/3586331-detectexposures). This method has three parameters:

#### Exposure Configuration

The exposure configuration defines the configuration for the Apple scoring of exposures. In our case we ignore most of the scoring methods and only provide the thresholds for the duration at attenuation buckets. The thresholds for the attenuation buckets are loaded from our [config server](https://github.com/DP-3T/dp3t-config-backend-ch/blob/master/dpppt-config-backend/src/main/java/org/dpppt/switzerland/backend/sdk/config/ws/model/GAENSDKConfig.java). This allows us to group the duration of a contact with another device into three buckets regarding the measured attenuation values that we then use to detect if the contact was long enough and close ennough.
The [ENExposureConfiguration](https://developer.apple.com/documentation/exposurenotification/enexposureconfiguration) defines the configuration for the Apple scoring of exposures. In our case we ignore most of the scoring methods and only provide [attenuationDurationThresholds](https://developer.apple.com/documentation/exposurenotification/enexposureconfiguration/3601128-attenuationdurationthresholds), the thresholds for the duration at attenuation buckets. The thresholds for the attenuation buckets are loaded from our [config server](https://github.com/DP-3T/dp3t-config-backend-ch/blob/master/dpppt-config-backend/src/main/java/org/dpppt/switzerland/backend/sdk/config/ws/model/GAENSDKConfig.java). This allows us to group the duration of a contact with another device into three buckets regarding the measured attenuation values that we then use to detect if the contact was long and close enough.
To detect an exposure the following formula is used to compute the exposure duration:
```
durationAttenuationLow * factorLow + durationAtttenuationMedium * factorMedium
Expand All @@ -32,12 +32,12 @@ If this duration is at least as much as defined in the triggerThreshold a notifi

#### Diagnosis key URLs

We need to unzip the file which we got from our backend and save them locally and pass the local urls to the Framework. Unlike Andorid on iOS we can't just pass the difference from last detection but we have to pass the every key of a day everytime we do a detection.
We need to unzip the file which we got from our backend, store the key file (.bin) and signature file (.sig) locally and pass the local urls to the EN API. Unlike Android, on iOS we can't just pass the difference from last detection but we have to pass the every key of a day everytime we do a detection.

#### Completion Handler

The completionHandler is called with a ENExposureDetectionSummary. That allows us to check if the exposure limit for a notification was reached by checking the minutes of exposure per attenuation window. The duration per window has a maximum of 30min, longer exposures are also returned as 30min of exposure.
The completionHandler is called with a [ENExposureDetectionSummary](https://developer.apple.com/documentation/exposurenotification/enexposuredetectionsummary). That allows us to check if the exposure limit for a notification was reached by checking the minutes of exposure per attenuation bucket. The duration per bucket has a maximum of 30min, longer exposures are also returned as 30min of exposure.

#### Rate limit

We are only allowed to call provideDiagnosisKeys() 20 times within 24h. Because we check for every of the past 10 days individually, this allows us to check for exposure twice per day. These checks happen after 6am and 6pm (swiss time) when the BackgroundTask is scheduled the next time or the app is opened. All 10 days are checked individually and if one fails it is retried on the next run. No checks are made between midnight UTC and 6am (swiss time) to prevent exceeding the rate limit per UTC day.
We are only allowed to call [detectExposures()](https://developer.apple.com/documentation/exposurenotification/enmanager/3586331-detectexposures) 20 times within 24h. Because we check for every of the past 10 days individually, this allows us to check for exposure twice per day. These checks happen after 6am and 6pm (swiss time) when the BackgroundTask is scheduled the next time or the app is opened. All 10 days are checked individually and if one fails it is retried on the next run. No checks are made between midnight UTC and 6am (swiss time) to prevent exceeding the rate limit per 24h.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ DP3T-SDK is available through [Cocoapods](https://cocoapods.org/)

```ruby

pod 'DP3TSDK', => '1.0.1'
pod 'DP3TSDK', => '1.0.2'

```

Expand Down Expand Up @@ -145,7 +145,7 @@ DP3TTracing.iWasExposed(onset: Date(), authentication: .none) { result in
```

### Sync with backend for exposed user
The SDK does not automatically sync with the backend for new exposed users. The app is responsible for fetching the new exposed users as it sees fit (periodically or via user input):
The SDK automatically syncs with the backend for new exposed users by scheduling a background task.
```swift
DP3TTracing.sync() { result in
// Handle result here
Expand All @@ -169,6 +169,8 @@ The SDK supports iOS 13 Background tasks. To enable them the app has to support
</array>
```

If a DP3TBackgroundHandler was passed to the SDK on initialisation it will be called on each background task execution by the SDK.

## License

This project is licensed under the terms of the MPL 2 license. See the [LICENSE](LICENSE) file.
2 changes: 2 additions & 0 deletions SampleApp/DP3TSampleApp/ControlViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,8 @@ extension ControlViewController: UITextFieldDelegate {
private extension TrackingState {
var stringValue: String {
switch self {
case .initialization:
return "initialization"
case .active:
return "active"
case let .inactive(error):
Expand Down
23 changes: 19 additions & 4 deletions Sources/DP3TSDK/Background/DP3TBackgroundTaskManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,27 @@ class DP3TBackgroundTaskManager {

private let serviceClient: ExposeeServiceClientProtocol

private let tracer: Tracer

init(handler: DP3TBackgroundHandler?,
keyProvider: DiagnosisKeysProvider,
serviceClient: ExposeeServiceClientProtocol) {
serviceClient: ExposeeServiceClientProtocol,
tracer: Tracer) {
self.handler = handler
self.keyProvider = keyProvider
self.serviceClient = serviceClient
self.tracer = tracer

NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
}

deinit {
NotificationCenter.default.removeObserver(self)
}

/// Register a background task
func register() {
logger.trace()
defer {
scheduleBackgroundTask()
}
guard !Self.didRegisterBackgroundTask else { return }
Self.didRegisterBackgroundTask = true

Expand All @@ -49,6 +56,10 @@ class DP3TBackgroundTaskManager {
}
}

@objc func appDidEnterBackground(){
scheduleBackgroundTask()
}

private func handleBackgroundTask(_ task: BGTask) {
logger.trace()

Expand Down Expand Up @@ -95,6 +106,10 @@ class DP3TBackgroundTaskManager {

private func scheduleBackgroundTask() {
logger.trace()
guard tracer.isAuthorized else {
logger.log("skipping schedule because ENManager is not authorized")
return
}
let taskRequest = BGProcessingTaskRequest(identifier: DP3TBackgroundTaskManager.taskIdentifier)
taskRequest.requiresNetworkConnectivity = true
do {
Expand Down
54 changes: 33 additions & 21 deletions Sources/DP3TSDK/DP3TSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class DP3TSDK {

let synchronizer = KnownCasesSynchronizer(matcher: matcher, service: service, descriptor: applicationDescriptor)

let backgroundTaskManager = DP3TBackgroundTaskManager(handler: backgroundHandler, keyProvider: manager, serviceClient: service)
let backgroundTaskManager = DP3TBackgroundTaskManager(handler: backgroundHandler, keyProvider: manager, serviceClient: service, tracer: tracer)

self.init(applicationDescriptor: applicationDescriptor,
urlSession: urlSession,
Expand Down Expand Up @@ -131,7 +131,7 @@ class DP3TSDK {
self.backgroundTaskManager = backgroundTaskManager
self.defaults = defaults

self.state = TracingState(trackingState: .stopped,
self.state = TracingState(trackingState: .initialization,
lastSync: defaults.lastSync,
infectionStatus: InfectionStatus.getInfectionState(from: exposureDayStorage),
backgroundRefreshState: UIApplication.shared.backgroundRefreshStatus)
Expand Down Expand Up @@ -185,28 +185,40 @@ class DP3TSDK {
}
OperationQueue().addOperation(outstandingPublishOperation)

// Skip sync when tracing is not active
if self.state.trackingState != .active {
log.error("Skip sync when tracking is not active")
callback?(.skipped)
return
}
let sync = {
// Skip sync when tracing is not active
if self.state.trackingState != .active {
self.log.error("Skip sync when tracking is not active")
callback?(.skipped)
return
}

group.enter()
var storedResult: Result<Void, DP3TTracingError>?
synchronizer.sync { result in
storedResult = result
group.leave()
group.enter()
var storedResult: SyncResult?
self.synchronizer.sync { result in
storedResult = result
group.leave()
}
group.notify(queue: .main) { [weak self] in
guard let self = self else { return }
switch storedResult! {
case .success:
self.state.lastSync = Date()
callback?(.success)
case .skipped:
callback?(.skipped)
case let .failure(error):
callback?(.failure(error))
}
}
}
group.notify(queue: .main) { [weak self] in
guard let self = self else { return }
switch storedResult! {
case .success:
self.state.lastSync = Date()
callback?(.success)
case let .failure(error):
callback?(.failure(error))

if self.state.trackingState == .initialization {
tracer.addInitialisationCallback {
sync()
}
} else {
sync()
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/DP3TSDK/DP3TTracing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.1"
public static let frameworkVersion: String = "1.0.2"

/// sets global parameter values which are used throughout the sdk
public static var parameters: DP3TParameters {
Expand Down
22 changes: 18 additions & 4 deletions Sources/DP3TSDK/DP3TTracingState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public enum InfectionStatus {

/// The tracking state of the bluetooth and the other networking api
public enum TrackingState: Equatable {
/// The tracker is not fully initialized
case initialization
/// The tracking is active and working fine
case active
/// The tracking is stopped by the user
Expand All @@ -45,8 +47,8 @@ public enum TrackingState: Equatable {

public static func == (lhs: TrackingState, rhs: TrackingState) -> Bool {
switch (lhs, rhs) {
case (.active, .active):
return true
case (.active, .active): fallthrough
case (.initialization, initialization): fallthrough
case (.stopped, stopped):
return true
case let (.inactive(lhsError), .inactive(rhsError)):
Expand All @@ -70,11 +72,23 @@ public struct TracingState {
}

/// Result of a sync
public enum SyncResult {
public enum SyncResult: Equatable {
/// Sync was successful
case success
/// An error occured
case failure(_ error: DP3TTracingError)
/// tracing is not active / sdk is still be in initialization phase
/// tracing is not active / sdk is still be in initialization phase / sync is defered due to ratelimit
case skipped

public static func == (lhs: SyncResult, rhs: SyncResult) -> Bool {
switch (lhs, rhs) {
case (.success, .success),
(.skipped, .skipped):
return true
case let (.failure(lhsError), .failure(rhsError)):
return lhsError.localizedDescription == rhsError.localizedDescription
default:
return false
}
}
}
8 changes: 6 additions & 2 deletions Sources/DP3TSDK/Networking/KnownCasesSynchronizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class KnownCasesSynchronizer {
}

/// A callback result of async operations
typealias Callback = (Result<Void, DP3TTracingError>) -> Void
typealias Callback = (SyncResult) -> Void

/// Synchronizes the local database with the remote one
/// - Parameters:
Expand Down Expand Up @@ -244,7 +244,11 @@ class KnownCasesSynchronizer {
callback?(.failure(lastError))
} else {
self.logger.log("finishing sync successful")
callback?(.success(()))
if totalNumberOfRequests != 0 {
callback?(.success)
} else {
callback?(.skipped)
}
}

DP3TTracing.activityDelegate?.syncCompleted(totalRequest: totalNumberOfRequests, errors: occuredErrors)
Expand Down
Loading

0 comments on commit 69308ea

Please sign in to comment.