This code project uses the ExposureNotification framework to build a sample app that lets people know when they have come into contact with someone who meets a set of criteria for a case of COVID-19. When using the project as a reference for designing a notification app, you can define the criteria for how the framework determines whether the risk is high enough to report to the user.
The sample app includes code to simulate server responses. When building an exposure notification app based on this project, create a server environment to provide diagnosis keys and exposure criteria, and add code to your app to communicate with this server. If the app you build operates in a country that authenticates medical tests for COVID-19, you may need to include additional network code to communicate with those authentication services.
Users must explicitly authorize an app to participate in exposure notification. The ENManager class provides information on the user’s authorization status and requests authorization. The app creates a singleton ENManager object, using its own class to manage the object’s lifetime.
Each time the app launches, it checks to see if the user has authorized notifications. If the user hasn’t authorized the app, it displays a user interface asking the user to authorize the service.
When the framework calls the completion handler, the app checks to see whether the user authorized exposure notifications. If the user hasn’t done so, the app displays a different screen alerting the user to the importance of opting into the behavior, and offers the user a second opportunity to authorize the app. The user can still decline.
Store User Data Locally
The app stores information about test results and high-risk exposures in the user defaults directory. The local data is private and stays on the device.
A custom property wrapper transforms data between its native format and a JSON-formatted equivalent, reads and writes data in the user defaults dictionary, and posts notifications to the app when local data changes. The LocalStore class manages the user’s private data, defined as a series of properties that all use this property wrapper.
The app defines its own data structures for any data it persists. For example, a test result records the date the user took the test, and whether the user shared this data with the server. This information is used to populate the user interface.
Share Diagnosis Keys with the Server
A user with a diagnosis for COVID-19 can upload diagnosis keys to the server. Each instance of the app periodically downloads diagnosis keys to search the device’s private interaction data for matching interactions.
This project simulates a remote server with which the app communicates. There is a single Server object in the app that stores the received diagnosis keys and provides them on demand. The sample server does not partition the data by region. It maintains a single list of keys, and provides the entire list upon request.
As with the local store, this local server stores the data in JSON format, using the same Persisted property wrapper.
Ask Users to Share COVID-19 Indicators
The sample app demonstrates a strategy in which a recognized medical authority tested the user and found positive COVID-19 indicators. The sample app provides a way for users to enter an authentication code, but doesn’t submit this data to an authentication service, so all codes automatically pass.
When the user provides information about a positive test result, the app records the test result in the local store and asks the user to share it. To share the result, the app needs to get a list of diagnosis keys and send the list to the server. To get the keys, the app calls the singleton ENManager object’s getDiagnosisKeys(completionHandler:) method, as shown in the code below.
Each time the app calls this method, the user must authorize the transaction. The framework then returns the list of keys to the app. The app sends those keys as-is to the server and then updates the test record to indicate that it was shared.
The sample app’s server implementation appends the keys onto a list it maintains, skipping any keys that are already there. It stores the keys sequentially so that the app can request just the keys it hasn’t received before.
Create a Background Task to Check for Exposure
The app uses a background task to periodically check whether the user may have been exposed to an individual with COVID-19. The app’s Info.plist file declares a background task named com.example.apple-samplecode.ExposureNotificationSampleApp.exposure-notification. The BackgroundTask framework detects apps that contain the Exposure Notification entitlement and a background task that ends in exposure-notification. The operating system automatically launches these apps when they aren’t running and guarantees them more background time to ensure that the app can test and report results promptly.
The app delegate schedules the background task:
First, the background task provides a handler in case it runs out of time. Then it calls the app’s detectExposures method to test for exposures. Finally, it schedules the next time the system should execute the background task.
The remaining sections describe how the app obtains the set of diagnosis keys and submits them to the framework for evaluation.
Download Diagnosis Keys
The app downloads diagnosis keys from the server to pass to the framework, starting with the first key the app hasn’t downloaded before. This design ensures that the app checks each diagnosis key only once on any given device.
The app needs to provide signed key files to the framework. The app asks the server for the URLs of any key files that the server generated after the last file that the app checked. After receiving the URLs from the server, the app uses a dispatch group to download the files to the device.
Finally, the app creates an array of the local URLs for the downloaded files.
Configure Criteria to Estimate Risk
The framework will compare locally saved interaction data against the diagnosis keys provided by the app. When the framework finds a match, it calculates a risk score for that interaction based on a number of different factors, such as when the interaction took place and how long the devices were in proximity to each other.
To provide specific guidance to the framework about how risk should be evaluated, the app creates an ENExposureConfiguration object. The app requests the criteria from the Server object, which creates and returns an ENExposureConfiguration object as shown below. The sample configuration has placeholder data that evaluates any interaction as risky, so the framework returns all interactions that match the diagnostic keys.
Submit Diagnosis Keys to the Framework
After downloading the key files, the app performs the search for exposures using a series of asynchronous steps. First, the app requests the criteria from the server, which calls into the code shown in the Configure Criteria to Estimate Risk section above. Then the app calls the ENManager objects’s detectExposures(configuration:diagnosisKeyURLs:completionHandler:) method, passing the criteria and the URLs for the downloaded key files. This method returns a summary of the search results.
Finally, the app calls its finish method to complete the search. The finish method updates the local store with the new data, including any exposures, the date and time the app executed the search, and the index for the key file to check next time.