Skip to content

Commit

Permalink
Merge pull request #192 from DP-3T/develop
Browse files Browse the repository at this point in the history
new Version
  • Loading branch information
stmitt authored Jul 17, 2020
2 parents 7cd50c8 + 4ab9c20 commit c9f2db9
Show file tree
Hide file tree
Showing 13 changed files with 336 additions and 74 deletions.
29 changes: 29 additions & 0 deletions .github/workflows/deploy_to_cocoapods.yml
Original file line number Diff line number Diff line change
@@ -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 }}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
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.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
Expand Down
38 changes: 13 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand All @@ -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.

<p align="center">
<img src="SampleApp/screenshots/1.png" width="256">
Expand All @@ -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:<br/>- `numberOfHandshakes` : `Int` <br /> - `trackingState` : `TrackingState` <br /> - `lastSync` : `Date` <br /> - `infectionStatus`:`InfectionStatus`<br /> - `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
Expand All @@ -74,7 +74,7 @@ DP3T-SDK is available through [Cocoapods](https://cocoapods.org/)

```ruby

pod 'DP3TSDK', => '1.0.2'
pod 'DP3TSDK', => '1.1.0'

```

Expand Down Expand Up @@ -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
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
</array>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>User facing text justifying bluetooth usage</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>User facing text justifying bluetooth usage</string>
```

### Checking the current tracing status
```swift
Expand Down Expand Up @@ -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
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>org.dpppt.exposure-notification</string>
<string>org.dpppt.exposure-notification</string>
<string>org.dpppt.refresh</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>processing</string>
<array>
<string>processing</string>
<string>fetch</string>
</array>
```

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

Expand Down
1 change: 1 addition & 0 deletions SampleApp/DP3TSampleApp/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>org.dpppt.exposure-notification</string>
<string>org.dpppt.refresh</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
Expand Down
86 changes: 72 additions & 14 deletions Sources/DP3TSDK/Background/DP3TBackgroundTaskManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand All @@ -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()
}

Expand All @@ -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)
}
}
}
13 changes: 9 additions & 4 deletions Sources/DP3TSDK/Background/OutstandingPublishOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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.2"
public static let frameworkVersion: String = "1.1.0"

/// sets global parameter values which are used throughout the sdk
public static var parameters: DP3TParameters {
Expand Down
13 changes: 11 additions & 2 deletions Sources/DP3TSDK/Matching/ExposureNotificationMatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,24 @@ 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
exposureSummary = summary
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)
Expand Down
Loading

0 comments on commit c9f2db9

Please sign in to comment.