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.

Prerequisites

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

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 support.

  • Google Play Services.

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 (see Force-stop handling).

There are three ways of initializing WorkManager:

Schedule periodic tasks

The app must ensure that the jobs are enqueued properly in the following situations:

At the same time, you should stop any enqueued job when the Exposure Notification API is disabled:

  • When calling stop() (such as when the user toggles a switch in your app).
  • When the ACTION_SERVICE_STATE_UPDATED broadcast is received with EXTRA_SERVICE_STATE equal to false.

In the example that follows, we schedule a unique periodic Worker to run every 12 hours. You may set your frequency to a different value, based on 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 workRequest = PeriodicWorkRequestBuilder<ProvideDiagnosisKeysWorker>(12, TimeUnit.HOURS)
    .setConstraints(constraints)
    .build()

// Use a unique work to avoid multiple workers.
workManager.enqueueUniquePeriodicWork(
    PROVIDE_KEY_WORK_NAME,
    ExistingPeriodicWorkPolicy.KEEP,
    workRequest
)

Use ExistingPeriodicWorkPolicy.KEEP to avoid rescheduling the work everytime the app enqueues it. Consider ExistingPeriodicWorkPolicy.REPLACE only when there is a need to stop the ongoing or pending periodic work and schedule it again.

Perform periodic tasks

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 ProvideDiagnosisKeysWorker(
    context: Context,
    workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters) {

    override suspend fun doWork(): Result {
        return try {
            // Important: ensure worker times out when using a FGS to avoid long-running jobs.
            withTimeout(TimeUnit.MINUTES.toMillis(10)) {
                // Skip work if API is not enabled or the Worker has been stopped.
                if (isEnabled && !isStopped()) {
                    // Make the worker run in a Foreground Service
                    setForeground(ForegroundInfo(NOTIFICATION_FETCH_ID, fetchNotification))
                    // Fetch diagnosis key files from your server
                    // and provide them 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 ProvideDiagnosisKeysWorker 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.

Also, it's important to ensure the worker handles exceptions properly and is not "stuck" (specially when using a Foreground Service). The snippet, earlier in this section, shows a simplified version. For more-detailed information check:

Best practices for security

Consider the following high-level security best practices when building an app that uses the Exposure Notifications API.

Encryption

Encryption is critical to protecting the confidentiality of data in your app. Consider the following encryption-related best practices when creating your app.

Opt out of cleartext traffic

To ensure your app sends data over HTTPS/SSL and not accidentally over HTTP (where it could be intercepted by an attacker), edit your AndroidManifest.xml file to include a reference to the network_security_config.xml resource. This is shown in the following code sample:

<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
    <application android:networkSecurityConfig="@xml/network_security_config"
                    ... >
        ...
    </application>
</manifest>

The network_security_config.xml resource contains the rules to enforce:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">secure.example.com</domain>
    </domain-config>
</network-security-config>

By setting cleartextTrafficPermitted to false, it will help ensure your app sends data over HTTPS where possible. For more information, see this article on network security configuration.

Validate TLS certificates

Check that your certificates are validated correctly. If a validation fails, do not form a connection to a server that is providing an invalid certificate. Ignoring certificate validation creates TLS interception risks for your users.

Not recommended — do not do this

// This is an example of poorly-implemented certificate validation.

try {
      URL url = new URL("https://wikipedia.org");
      URLConnection urlConnection = url.openConnection();
      InputStream in = urlConnection.getInputStream();
      copyInputStreamToOutputStream(in, System.out);
    }

// DO NOT do the following! This ignores invalid or malicious certificates.
// ALWAYS handle this exception.
catch (SSLHandshakeException e) {continue};

Follow the recommended steps outlined in the Android security documentation for best practices on managing SSL/TLS in your application.

Avoid embedded secrets

Don’t store encryption secrets directly in your code. It allows anyone to unpack and read the secret, and use it to gain access to resources that would otherwise be protected. This includes initialization vectors (IVs), shared secret keys for symmetric encryption, and secret keys used to encrypt local storage.

Instead of storing encryption secrets in your code, use shared preferences or rely on Android’s keystore system. The Jetpack security library allows you to easily and securely encrypt shared preferences and files.

Third-party code

When using third-party code in your app:

  • Consider the attack surface it presents.
  • Use any available mitigations that reduce risk.
  • Identify and address any known vulnerabilities in third-party code.

Keeping third-party code up to date ensures your app includes any necessary patches to address previously identified vulnerabilities. If the third-party developer has a security bulletin process for providing information on known vulnerabilities along with instructions on how to address them (such as mitigations and workarounds or patching to the latest version), review this regularly and address these issues. This can reduce the risk of attackers exploiting known vulnerabilities.

Application security fundamentals

Besides aspects of application security that are specific to COVID-19 exposure notifications applications, it’s important to keep in mind potential security and privacy problems that apply to all Android apps.

In addition to the issues described in the following sections, consider reviewing this overview of common Android application security vulnerabilities reported to our Google Play Security Reward program.

Unprotected app components

You can keep Android app components—such as activities, services, and content providers—internal to apps, or export them for interactions with other apps. Whether a component is exported depends on component properties, which vary between different component types. Some of these rules can be subtle or unexpected, so it’s good to review the export rules for all component types declared in the app manifest file. To limit the attack surface of your app, export as few components as possible.

Intent Redirection Vulnerabilities can allow malicious apps to access private app components or files within your app. Review arguments that your app passes to exported app components to ensure they cannot be abused. Whenever possible, make app components private by setting android:exported=”false” in your manifest.

Implicit intents

Use of implicit intents for sensitive communication and improper verification of intents by broadcast receivers are also common vulnerabilities. Intents that are not limited to a target package are broadcast to the whole system, allowing malicious apps on the same device to intercept sensitive data or maliciously manipulate data passed to other apps. Apps that receive implicit broadcasts may receive maliciously manipulated intent arguments, causing the app to perform unintended actions.

To learn how to protect your app from risks associated with implicit intents, review the examples and mitigations in CWE-927. To learn how to protect your app against risks associated with implicit broadcasts, review the examples and mitigations in CWE-925. For more information on implicit intents and implicit broadcasts and the risks of using each, see Android app vulnerability classes.

Private file access

Be cautious about storing files in locations where other apps can access them. Before the introduction of scoped storage in Android 10, a device shared its external storage between apps and allowed all files placed there to be read and written to by other apps. Even when files are stored on internal storage, it’s possible for other apps to view their contents if these private files are copied to external storage for backup, or even just temporarily for sharing with other apps. Turn off backups for your app, and use only internal storage for sensitive files.

Likewise, Directory Traversal Vulnerabilities can occur when a malicious app directs your app to read from or write to your app’s internal storage. This can happen when another app can specify a path for writing a shared file. This can also happen when your app accepts untrusted ZIP files from other apps.

Incorrect URL verification

A common practice is to implement checks to validate whether a given URL belongs to a domain or URL that you trust. This check is surprisingly complicated and can cause issues in very subtle ways. HackerOne hosts a list of techniques commonly used to bypass such checks. If your app tries to verify domains, make sure it passes these checks.

Cross-app scripting

Be careful when loading URLs obtained from untrusted sources using WebView objects. If you enable JavaScript in your WebView object, URLs beginning with the javascript: tag can cause your WebView object to evaluate untrusted JavaScript code. If your WebView object enables local file access, malicious URLs can point to your WebView’s cookie database. Some of these cookies might already contain untrusted scripts, which the malicious URLs can use to steal the rest of the cookie database. Pay special attention to symbolic links when validating file paths; it may not be sufficient to check the string contents of a file path.

To minimize these risks, don’t enable file access or JavaScript in WebViews that do not need these features.

Access control

Ensure you have appropriate authorization and authentication controls in place to restrict access to sensitive data. To do this, follow the principle of least privilege to ensure that access to data and functionality is granted to only those that require it. For example, when a request comes in to access a user's profile that may contain sensitive personal information, use server-side controls to check whether the requestor is authorized to access this data. To learn more, see OWASP's Access Control Cheat Sheet.

Privacy considerations

To create an effective and secure exposure notifications application, you must guard against fake infection data, location spoofing, authentication flaws, insecurely anonymizing data, and other vulnerabilities. The Google Exposure Notification API handles many of these considerations. Use the Exposure Notification API wherever possible rather than reimplementing the functionality it provides, even if reimplementing seems trivial.

Additional resources

Review the following resources for more information on how to secure your app:

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 determine the state of the device and guide users through any manual settings changes needed to enable ENS you can use the getStatus() method (added in v1.7), which returns a variety of ExposureNotificationStatus constants These constants are specific to the user's version of Android. For example, if the getStatus() method returns LOCATION_DISABLED, then they need to manually enable location.

Additional resources

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

Android reference design

Check out our open-source project on GitHub.

The app implements all relevant use cases, such as onboarding users, generating TEKs and generating user notifications. It's the same code base used by EN Express.

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

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.