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:
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. We also recommend you enroll in the Play Services Public Beta Program.
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 Temporary 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 support.
Integrate the Exposure Notifications API into your app
Integrating the Exposure Notifications API involves the following steps:
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 callstart()
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
ExposureWindow
s 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.
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.
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.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:
The app handles the uploading and downloading of TEKs, and periodically provides the files with the confirmed TEKs that follow the file format.
The Exposure Notifications system matches the key and calculates possible exposure, returning it to the app.
The app evaluates whether the exposure data returned by the Exposure Notification system indicates any meaningful exposures as explained in Get exposure risk.
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:
- If you're using WorkManager with the default configuration, then scheduled work will automatically be rescheduled.
- If you're using a custom
configuration,
you must initialize WorkManager in the
Application.onCreate()
method. - If you are using WorkManager's on-demand
initialization
(implementing the
Configuration.Provider
interface in yourApplication
class), you must initialize WorkManager in theApplication.onCreate()
method, callingWorkManager.getInstance(Context)
to guarantee that WorkManager is initialized during application start-up.
Schedule periodic tasks
The app must ensure that the jobs are enqueued properly in the following situations:
- When the application starts and the ExposureNotification API is enabled for your app.
- When the
start()
method succeeds after the user authorizes your app. - When the
ACTION_SERVICE_STATE_UPDATED
broadcast is received withEXTRA_SERVICE_STATE
equal to true.
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 withEXTRA_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 Worker
s:
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:
Check if the client
isEnabled
; return failure and skip the work if it is not.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:
The API error handling section.
How the Exposure Notification Express handles timeouts.
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:
- Exposure Notifications: Risks and Mitigations FAQ
- Implement security by design for your apps
- Android app vulnerability classes
- App security best practices
- Work with data more securely
- Security with HTTPS and SSL
- Vulnerability Disclosure Program Guide
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.