Exposure Notifications implementation guide

This guide includes detailed instructions for building an app that uses the Exposure Notifications API to notify users of possible exposure to confirmed COVID-19 cases.

An alternate option is to create an EN Express implementation.

Prerequisites

To integrate the Exposure Notifications API into your app, you need the following:

  • Exposure Notifications key server. This is required for downloading and uploading the necessary keys. See guidance on server-side implementation for details.

  • An allowlisted Google account.

  • Exposure Notifications debug mode enabled on the device. This is required for testing the Exposure Notifications API during development.

  • IDE for Android development. The recommended IDE for Android development is Android Studio, which includes all tools needed to build Android apps.

  • Android device. The Exposure Notifications API uses the Bluetooth Low Energy (BLE) protocol to exchange Technical Exposure Keys (TEKs). Bluetooth (BT) isn't available on the Android Emulator, so you need a physical device to test your implementation.

    To use the Exposure Notifications API, a device must support the following minimum requirements:

    • Android 6.0 (API level 23). API level 21 is also partially supported. See Exposure Notifications API architecture for more information.

    • BLE protocol.

    • The latest version of Google Play services.

Sample app

See the Android reference design app on GitHub for a sample Exposure Notifications app written in Java.

This app implements all relevant use cases, such as onboarding users, generating TEKs, and generating user notifications.

To file a bug or ask questions related to the Android reference design app, open an issue on GitHub.

Integrate the Exposure Notifications API into your app

Integrating the Exposure Notifications API involves the following steps:

  1. Set up the AndroidManifest

  2. Authorize Exposure Notifications

  3. Download diagnosis keys from a server

  4. Provide diagnosis keys to the Exposure Notifications system

  5. Get the exposure risk

  6. Notify users about possible exposure

  7. Upload TEKs

Set up the AndroidManifest

In your AndroidManifest.xml file, state that BLE is required to run, and declare permissions for BT and internet. BT is required for scanning and advertising Rotating Proximity Identifiers (RPIs), and internet is required for uploading and downloading TEKs.

<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<uses-feature android:name="android.hardware.bluetooth" />

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BLUETOOTH" />

Next, declare Exposure Notifications broadcast receivers. These receivers notify the client app both after a batch of keys is processed and when the Exposure Notifications system for the app has been disabled.

<receiver android:name=".nearby.ExposureNotificationBroadcastReceiver"
    android:permission="com.google.android.gms.nearby.exposurenotification.EXPOSURE_CALLBACK"
    android:exported="true">
    <intent-filter>
        <action android:name=
            "com.google.android.gms.exposurenotification.ACTION_EXPOSURE_STATE_UPDATE" />
        <action android:name=
            "com.google.android.gms.exposurenotification.ACTION_EXPOSURE_NOT_FOUND" />
        <action android:name=
            "com.google.android.gms.exposurenotification.SERVICE_STATE_UPDATED" />
    </intent-filter>
</receiver>

Authorize Exposure Notifications

The ExposureNotificationClient, part of the nearby module, is the main entry point of the API.

Create an instance using nearby.getExposureNotificationClient():

val client = nearby.getExposureNotificationClient(context)

Your app must prompt users to authorize the use of Exposure Notifications on the device. To request user consent, call the start() method, which returns any of the following status messages:

  • Success once the app is authorized by the user and the API is enabled.
  • Failure with RESOLUTION_REQUIRED if the user has not authorized the app. In this case, resolve the issue and call start() again.
  • Failure without resolution if there was an error while trying to enable the API. For details on error messages, see ExposureNotificationStatusCodes.
client.start()
   .addOnSuccessListener {
       // The app is authorized to use the Exposure Notifications API.
       // The Exposure Notifications API started to scan (if not already doing so).
   }
   .addOnFailureListener { exception ->
       if (exception is ApiException) {
           val status: Status = exception.status
           if (status.hasResolution()) {
               status.startResolutionForResult(activity, REQUEST_START_CODE)
           } else {
               // Handle other status.getStatusCode().
           }
       }
   }

The result of the resolution is returned in the onActivityResult of the activity used to start the resolution.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
   super.onActivityResult(requestCode, resultCode, data)
   when (requestCode) {
       REQUEST_START_CODE -> {
           if (resultCode == Activity.RESULT_OK) {
               // The resolution is solved (for example, the user gave consent).
               // Start ExposureNotificationsClient again.
           } else {
               // The resolution was rejected or cancelled.
           }
       }
   }
}

For more information on extracting status information from error codes, check Error Handling.

It is safe to call start() even if the API is enabled and the app is already authorized, but the API offers the isEnabled() method. This method verifies that the app is the current active app and that Exposure Notifications is enabled.

client.isEnabled.addOnSuccessListener { isEnabled ->
    if (isEnabled) {
        // Show API-enabled status.
    } else {
        // Show API disabled status or call start().
    }
}.addOnFailureListener { exception ->
    // Some conditions are not satisfied to use the API.
}

Download diagnosis keys from the server

Follow these requirements when downloading diagnosis keys from the server:

  • Download only required keys, which are:

    • Files that have not been already downloaded and shared with the Exposure Notifications system.

    • Files not older than the day the app was enabled.

  • When the app has been offline for a long period of time, consider using a "catch up" mechanism that wraps multiple files into fewer batched downloads.

  • When downloading the files from the internet-accessible server, store them inside the app-specific directory on internal storage. Optionally, you can create a subdirectory under the filesDir and delete the subdirectory after you've finished processing.

Provide diagnosis keys to the Exposure Notifications system

Once the application has downloaded the exposure files, call provideDiagnosisKeys() with the list of files to match TEKs for the possible exposure.

val exposureFiles: List<File> = ... // Files downloaded from the server.
client.provideDiagnosisKeys(exposureFiles)
    .addOnSuccessListener {
        exposureFiles.forEach { file ->
            file.delete()
        }
    }
    .addOnFailureListener {
        // Handle the error and retry periodic job.
    }

If the method calls onSuccessListener, it means the Exposure Notifications system accepted the files and verified the format and signature. Delete the provided files to reduce risks to user privacy.

In case of failure, check for the status code, retry the job, and consider notifying the users. For more information about issues with verification, see Verification.

Get the exposure risk

The Exposure Notifications system compares locally stored TEKs against the provided diagnosis keys. To retrieve the exposure data, the system uses the concept of the ExposureWindow.

The app can use the getExposureWindows() method to retrieve the list of ExposureWindows that provides the necessary insights to evaluate the exposure risk of the user.

client.getExposureWindows().addOnSuccessListener { windows ->
    if (windows.isEmpty()) {
      // No exposures were found.
    } else {
      // One or more exposures are found; therefore, calculate the risk.
    }
}.addOnFailureListener {
    // Some conditions are not satisfied to use the API.
}

For details, see Define meaningful exposures.

Notify users about possible exposure

As described in Set up the AndroidManifest, your app must declare one or more broadcast receivers. When the matching process to find exposures is completed, the Exposure Notifications system sends an ExposureNotificationClient.ACTION_EXPOSURE_STATE_UPDATED broadcast action (when at least one exposure was found) or ExposureNotificationClient.ACTION_EXPOSURE_NOT_FOUND (if no exposure was found).

To receive exposure state updates, the application must implement a broadcast receiver to handle these broadcast actions. If an exposure was found, show a notification to the user. For information about displaying this information to the user, see Risk exposure notification.

The following snippet shows an example of implementing the receiver using the v1.5 API mode.

class MainBroadcast : BroadcastReceiver() {

    companion object {
        const val EN_NOTIFICATION_ID = 101
    }

    override fun onReceive(context: Context, intent: Intent) {
        when (intent.action) {
            ExposureNotificationClient.ACTION_EXPOSURE_NOT_FOUND -> {
                // Optional: notify the user.
            }
            ExposureNotificationClient.ACTION_EXPOSURE_STATE_UPDATED -> {
                val infoIntent = PendingIntent.getActivity(
                    context,
                    EN_NOTIFICATION_ID,
                    Intent(context, MainActivity::class.java),
                    PendingIntent.FLAG_UPDATE_CURRENT
                )
                val channelId = // Get the created channel ID
                val builder = NotificationCompat.Builder(context, channelId)
                    .setContentIntent(infoIntent)
                    .setSmallIcon(...)
                    .setContentTitle(...)
                    .setContentText(...)
                    .setAutoCancel(true)
                    // Optional: Avoid the notification being cleared accidentally
                    // by users until it is explicitly opened.
                    .setOngoing(true)
                    // Ensure the notification is shown to the user.
                    .setPriority(NotificationCompat.PRIORITY_MAX)
                    // This is sensitive information, we recommend to use secret.
                    .setVisibility(NotificationCompat.VISIBILITY_SECRET)

                val manager = NotificationManagerCompat.from(context)
                manager.notify(EN_NOTIFICATION_ID, builder.build())
            }
            else -> {
                // Handle other actions if needed
            }
        }
    }
}

Upload Temporary Exposure Keys

When a user reports themselves as having tested positive for COVID-19, your app must perform specific actions to upload TEKs. The app is responsible for two of these steps, and the API assists with one of the steps.

  1. If applicable, verify against the server of your public health authority (PHA) that the user actually tested positive. Your app is responsible for this step. See Exposure Notifications verification server for more information.

  2. Retrieve the user’s TEKs from the last 14 days. The API assists you with this step.

    When collecting a user's TEKs for uploading, don't introduce restrictions in the number of TEKs per day or the expected value of TEK fields (for example, rollingPeriod). Assume they are variable and make your system resilient to change.

  3. Upload the keys (and any applicable metadata) to your diagnosis key server. Your app is responsible for this step. For more information on making network calls on Android, see our developer site documentation for Connectivity basics and Network Security Best Practices.

Retrieve the user's TEKs from the last 14 days

The getTemporaryExposureKeyHistory() method returns the user's TEKs from the last 14 days. Similar to the start() method, you must request a resolution before you can access the keys. This prompts the user for consent to retrieve the keys.

client.getTemporaryExposureKeyHistory()
       .addOnSuccessListener { temporaryExposureKeys: List<TemporaryExposureKey> ->
           // Keys were successfully retrieved. Call your own method to handle the upload.
       }
       .addOnFailureListener{ exception: Exception? ->
           if (exception is ApiException) {
                   val status: Status = exception.status
                   if (status.hasResolution()) {
                       status.startResolutionForResult(activity, REQUEST_HISTORY_CODE)
                   } else {
                       // Handle other status.getStatusCode().
                   }
               }
       }

The result of the resolution is returned in the onActivityResult of the activity used to start the resolution.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
   if (requestCode == REQUEST_HISTORY_CODE) {
       if (resultCode == Activity.RESULT_OK) {
           // The resolution was completed. Attempt to re-retrieve the keys again.
       } else {
           // There was an issue with the user accepting the prompt.
       }
   }
}

Once the user has provided consent, call the getTemporaryExposureKeyHistory() method again to asynchronously retrieve a List<TemporaryExposureKey> objects.

Best practices for background work

One of the key features of an Exposure Notifications app is to check for exposures and alert users when they've been exposed to a positively-diagnosed individual.

The app and the Exposure Notifications system must work together to share responsibility:

  1. The app handles the uploading and downloading of TEKs, and periodically provides the files with the confirmed TEKs that follow the file format.

  2. The Exposure Notifications system matches the key and calculates possible exposure, returning it to the app.

  3. The app evaluates whether the exposure data returned by the Exposure Notification system indicates any meaningful exposures as explained in Get exposure risk.

  4. The app handles the result as explained in Notify users about possible exposure.

Use WorkManager

To handle the periodic background fetch of these files, we highly recommend using Jetpack WorkManager. Specifically, use a foreground service job that displays a visible notification to the user to reduce the chance of falling into the rare bucket.

All scheduled work tasks are canceled if your app is force-stopped by the user or the device, though the Google Play services layer continues working. The Exposure Notifications system periodically checks to see if the active Exposure Notifications app has been force-stopped and restarts its process.

There are three ways of initializing WorkManager:

Schedule periodic tasks

Once the app is enabled to use the Exposure Notifications API, it should schedule a periodic task. In the example that follows, we schedule a Worker to run once a day, but ultimately the frequency is determined by your PHA requirements. While it's important to provide the latest info to the user, be conscious of the impact on the device battery.

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    // Consider other constraints.
    .build()
val fetchRequest = PeriodicWorkRequestBuilder<FetchWorker>(1, TimeUnit.DAYS)
    .setConstraints(constraints)
    .build()

// Use a unique work to avoid multiple workers.
// Use ExistingPeriodicWorkPolicy.REPLACE to ensure that the job is rescheduled
// as a workaround for https://issuetracker.google.com/166292069.
workManager.enqueueUniquePeriodicWork(
    FETCH_TAG,
    ExistingPeriodicWorkPolicy.REPLACE,
    fetchRequest
)

There are three types of Workers: ListenableWorker, Worker, and RxWorker. If you're using Kotlin, you can also use CoroutineWorker.

The following snippets use CoroutineWorker for readability.

class FetchWorker(
    context: Context,
    workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters) {

    override suspend fun doWork(): Result {
        // Make the worker run in a Foreground Service.
        setForeground(ForegroundInfo(NOTIFICATION_FETCH_ID, fetchNotification))

        return try {
            // Skip work if API is not enabled.
            if (isEnabled) {
                // Fetch export.zip files
                // and provide files to the Exposure Notifications API.
                Result.success()
            } else {
                Result.failure()
            }
        } catch (e: Exception) {
            // Handle exceptions, and retry or fail the worker.
            if (runAttemptCount < 3) {
                Result.retry()
            } else {
                Result.failure()
            }
        }
    }
}

The FetchWorker extends CoroutineWorker (or ListenableWorker for Java) and is responsible for the following main steps:

  1. Check if the client isEnabled; return failure and skip the work if it is not.

  2. Download diagnosis keys from the server

  3. Provide the diagnosis keys to the Exposure Notifications system.

Verify server export files

Invalid keys in export files can lead to high CPU and power consumption on the mobile phone, causes a bad user experience, and prevents your app from notifying end users about the exposures.

Read the External export files verification guide for more information on how to ensure your export files are working properly.

Locationless scanning in Android 11

The requirement to have location enabled has been a source of confusion for some users. Android 11 introduced a change that allows users to use the Exposure Notification system without having location enabled. This also requires a v1.6+ module as described in the release notes.

To check if the device supports locationless scanning, use the method: deviceSupportsLocationlessScanning(). This method was added to the SDK v1.6. You can find sample usage in the Android reference implementation.

Exposure Notifications Express overview

Google and Apple created a turnkey solution called Exposure Notifications Express (EN Express), which makes it easier for PHAs to support COVID-19 digital contact tracing efforts. Instead of building an Exposure Notifications app, PHAs can provide Google and Apple with a configuration file that includes instructions and content, including the messaging to users and the risk parameters for triggering an exposure notification. This information is used to generate a custom app for the PHA.

EN Express is based on our open-sourced sample application provided on GitHub. PHAs can still develop custom apps, and existing apps using the Exposure Notifications API are compatible with EN Express. For countries that have already created a custom app on Android, the availability of EN Express does not change how your current application works. EN Express works on all the same devices as before (that is, Android 6.0 and higher).

EN Express provides another option for PHAs to supplement their existing contact tracing operations with technology without compromising the Exposure Notifications project’s core tenets of user privacy and security. Visit our Help Center for more information on EN Express.

Additional resources

Refer to these additional resources for guidance on various aspects of the Exposure Notifications API.

Guidance on server-side implementation

Check out our open-source project on GitHub, Exposure Notification Reference Key Server.

The sample server implements required features such as:

  • Accepting and validating TEKs
  • Generating files that are downloaded by mobile devices
  • Sending public keys to devices

Guidance on verification server implementation

Check out our reference implementation for a verification server, also on GitHub.

Guidance on TEK validation

For information on some of the elements to consider when validating TEKs, see Recommended Temporary Exposure Key validation.

Detailed API documentation

For detailed information on the architecture, data structures, and other aspects of the API, see Exposure Notifications API.