The Exposure Notifications API is a joint effort between Apple and Google to provide the core functionality for building Android apps to notify users of possible exposure to confirmed COVID-19 cases.
For an overview of the goals of the system, see the COVID-19 information & resources page.
This guide shows you how to use the Exposure Notifications API to build Android apps that notify users of possible exposure to confirmed COVID-19 cases.
Note: This guide is released in advance of the SDK and is updated in parallel with subsequent SDK versions. Prepare your implementation cycle accordingly.
This guide contains the following sections:
- Architecture: This section describes how the system is distributed across different subsystems.
- Exposure Notifications API modes: This section describes the ExposureWindow and Legacy v1 modes of the API.
- Data structures and Methods: These sections describe how to interact with the API from your app.
- Broadcast receivers: This section describes how to define broadcast receivers in your Android manifest and how to respond to received broadcasts.
- Error handling: This section describes the error codes to check for when you call methods, provides suggestions about how to respond to the error codes, and how to handle other issues.
- Reference design: This section provides links to the project on GitHub, which provides examples about how to interface with the API.
- Glossary: This section provides definitions of terms specific to the Exposure Notifications API.
These sections provide overview material. If you need more detailed information about specific classes and methods, see the Exposure Notifications reference.
Architecture
The Exposure Notifications system spans several subsystems. This section describes this distribution.
Google Play services
Bluetooth functionality happens within on-device Google Play services.
Bluetooth functionality includes all broadcasts and scans for BLE beacons,
along with local database storage. Your app must have the
BLUETOOTH
and
INTERNET
permission in its manifest, but your app doesn't require and can't include
ACCESS_COARSE_LOCATION
,
ACCESS_FINE_LOCATION
,
nor
BLUETOOTH_ADMIN
.
For more information about restrictions on your app, see the API’s
Terms of Service.
For your app to work on all devices, set your minSdkVersion
to version 6.0
(API level 23) or higher. The framework works on some devices as low as
version 5.0 (API level 21), so using this version is an option to increase the
number of devices eligible. If you decide to set the minSdkVersion
to API
level 21, you should test your app on the most popular Android 5.0 devices in
your country to ensure that your app works as intended.
The complete list of the parts of the system that Google Play services handles is as follows:
Management of daily random keys:
- Generates daily random Temporary Exposure Keys and Rotating Proximity Identifiers (RPIs) based on them.
- Provides keys to the application for diagnosed users, which include an interval number that indicates the key date.
- Accepts keys and their associated metadata from the application for exposure detection. See the Exposure Key export file format and verification for more information.
- Stores key data in an on-device data store.
Management of Bluetooth broadcast and collection:
- Enables broadcast beacons.
- Scans for other beacons.
- Stores observed RPIs in an on-device data store.
- Identifies whether the user was in close contact with a confirmed case.
- Calculates and provides an exposure risk level to the app.
Presentation of consent requests to users at the following points:
- Performs this action the first time an app activates the system, before scanning and broadcasting begins.
Performs this action before user keys are provided to the app for upload to the internet-accessible server, after the user has been positively diagnosed with COVID-19.
Mobile app
Your app does the following:
Enable users to start and stop broadcast and scan functions.
Provide the files with the diagnosis keys and associated metadata (following the file format) from your internet-accessible server to Google Play services.
Retrieve keys from the on-device data store and submit them to your internet-accessible server after one of the following conditions is present:
A user has been confirmed by a medical provider to have tested positive.
A health authority authorized a user-initiated self-report based on symptoms (if your app supports this use case).
In both situations, the user must provide consent before the keys are provided to the app.
Schedule polls of your internet-accessible server for keys.
Register a receiver to receive broadcasts of the
com.google.android.gms.exposurenotification.ACTION_EXPOSURE_STATE_UPDATED
andcom.google.android.gms.exposurenotification.ACTION_EXPOSURE_NOT_FOUND
intents, and respond with a presentation of information to the user. See Broadcast receivers for more information.Show a notification to the user with instructions about what to do next when the user has been exposed to another user who has tested positive for COVID-19. For more information on how to provide this information, see Risk exposure notification.
Authenticated medical provider validation mechanism
Your app must provide functionality that confirms that the user has been positively diagnosed with COVID-19. This functionality controls the release of Temporary Exposure Keys stored on a device whose user is positively diagnosed. The released keys are sent to your app, which then uploads them to your internet-accessible server.
For more information about this part of the subsystem, see Exposure Notifications verification server.
Internet-accessible server
Your app must be able to communicate with an internet-accessible server that you own and that performs the following functions:
- Collect diagnosis keys from users who have been diagnosed with COVID-19.
- When polled, distribute diagnosis keys of confirmed cases to devices.
Exposure Notifications API modes
In v1.5 and higher, the Exposure Notifications API introduced ExposureWindow mode to provide enhanced risk calculation functionality.
We strongly recommend migrating to ExposureWindow mode. This mode allows you to separately view and revise or revoke matches from multiple days, while still leaving enough quota to update six times per day. Although the legacy v1 mode has more quota that can be used to partition matches by day, your app wastes the user's battery to do so and can even sometimes run out of quota.
Select the mode based on the provideDiagnosisKeys
method used:
To enable the
ExposureWindow
feature (thereby deactivating theExposureSummary
andExposureInformation
features), callprovideDiagnosisKeys(files)
with only the list of files.To enable the
ExposureSummary
andExposureInformation
features (thereby deactivating theExposureWindow
feature), callprovideDiagnosisKeys(files, token, config)
with a token.
The API is backward compatible with all versions: it activates non-breaking changes when a new version is available on the device, even when using the legacy v1 mode. Such changes would include, for example, the same-day release of keys and the ability to provide keys from multiple files with different signatures in one call.
ExposureWindow mode | Legacy v1 mode | |
---|---|---|
Token concept | Do not use tokens
Or | .
Use any token != TOKEN_A .Using different tokens groups keys in different groups. |
Obtain a summary risk score | getDailySummaries() (available in v1.6) |
getExposureSummary() . |
Get detailed exposure information | getExposureWindows() . |
getExposureInformation() . |
provideDiagnosisKeys quota |
|
|
Exposure Notifications API versions supporting this mode | In v1.5 and higher | In v1 and higher |
Note: We strongly recommend migrating to ExposureWindow mode for enhanced risk calculation functionality. |
Data structures
The API contains the data classes described in the following list.
Name and description | |||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Status |
This class contains codes that represent the state of the service on the device. For more information on how to use the information in this class, see Error handling. | ||||||||||||||||
TemporaryExposureKey |
The
|
||||||||||||||||
ExposureWindow
|
The ExposureWindow class stores a duration of up to 30 minutes,
during which beacons from a diagnosis key were observed. Each ExposureWindow
corresponds to a single diagnosis key. However, one key can lead to several
ExposureWindow s due to the 30-minute cutoffs between exposure
windows in a longer exposure, and the key itself is not exposed.
See getExposureWindows() for more
information.
|
||||||||||||||||
ScanInstance
|
In ExposureWindow mode, the
|
||||||||||||||||
DailySummariesConfig
|
The
|
||||||||||||||||
DailySummary
|
The
The data in a |
||||||||||||||||
ExposureSummaryData
|
The
See |
||||||||||||||||
DiagnosisKeysDataMapping
|
Mappings from diagnosis keys data to exposure data concepts returned by
the Exposure Notification API:
|
||||||||||||||||
ExposureConfiguration
Note: We strongly recommend migrating to ExposureWindow mode for enhanced risk calculation functionality.
|
In legacy v1 mode, the ExposureWindow s reported through
getExposureWindows() .
Configuration fields are as follows.
In legacy v1 mode, the following fields are used in the following
calculation to determine a
|
||||||||||||||||
ExposureSummary
Note: We strongly recommend migrating to ExposureWindow mode for enhanced risk calculation functionality.
|
In legacy v1 mode, the
The
Your app can present this information to users. |
||||||||||||||||
ExposureInformation
Note: We strongly recommend migrating to ExposureWindow mode for enhanced risk calculation functionality.
|
In legacy v1 mode, the
You can use this information to gauge a level of risk of the exposure to filter out low-risk interactions, such as sub-five-minute interactions with a low signal strength attenuation that occurred 13 days earlier. Each time this information is requested using
|
Methods
The API provides functionality with the use of the methods shown in the following list. These are provided in a roughly chronological order of usage by an app.
To enable asynchronous operations, return values from methods are wrapped in a
Task
object.
Name and description | |
---|---|
start() |
Tells Google Play services to start the broadcast and scan process.
The first time that this method is called after installation of the app
or each time after stop() is called, it prompts
Google Play services to display a dialog box, where the user is asked
to give permission to broadcast and scan. |
isEnabled() |
Indicates whether exposure notifications are enabled on the device. Note that this does not check whether Bluetooth or location settings are on. |
stop() |
Disables the broadcast and scan process. You can call this directly. It's also called when a user uninstalls the app. When it’s called as part of the uninstallation process, the database and keys are deleted from the device. |
deviceSupportsLocationlessScanning() |
Indicates whether the device supports locationless scanning. |
getStatus() |
Gets the current Exposure Notifications status and returns an
ExposureNotificationStatus value. |
getVersion() |
Returns a Long integer representing the version of the
Exposure Notification module installed on the user device. |
setDiagnosisKeysDataMapping() |
This function takes a DiagnosisKeysDataMapping object and sets the
diagnosis keys data mapping if it wasn't already changed recently.
If called twice within 7 days, the second call has no effect and
raises an exception with status code FAILED_RATE_LIMITED .
In v1.7 and higher, these settings are applied to previously calculated
|
getDiagnosisKeysDataMapping() |
This function returns the current DiagnosisKeysDataMapping .
|
getCalibrationConfidence() |
This function returns an int representing the calibration confidence value for
rssi_offset . Higher value means higher confidence (see
calibrationConfidence for possible values). |
getVariantOfConcern() |
This function returns an int representing the variant of concern.
variantOfConcern for possible values). |
getTemporaryExposureKeyHistory() |
Retrieves key history from the data store on the device to upload to your internet-accessible server. Calling this method prompts Google Play services to display a dialog that requests consent from the user to gather and upload their exposure keys. The keys that are returned depend on the API version. These key include the past 14 days, but not the current day’s key. (This is different for allowlisted accounts, which also return the current day’s key.) The consent granted by the user lasts for 24 hours. The dialog prompting for consent appears only once for every 24-hour period, regardless of how many times the app calls the method. In v1.7 and higher: Allowlisted apps support returning keys that
include the current day’s key, which is tagged with an expiration
date ( |
provideDiagnosisKeys() |
The
These keys must correspond to confirmed cases retrieved from your
internet-accessible server. Each batch (same
When using the ExposureWindow mode, all provided keys are accumulated
under the
token string used to identify a set
of keys in future calls to the API. It effectively determines how keys
accumulate over calls to provideDiagnosisKeys() :
Calls to this method are limited to six per day for
This quota applies per call, not per file or batch of files uploaded. In other words, a single call with multiple files or multiple batches counts as only one call. Days are defined as midnight to midnight of a device's UTC. filesDir
and delete the subdirectory after you've finish processing.
|
getExposureSummary()
|
In legacy v1 mode, the
This method does not support This method has no quota, and calling this method does NOT by itself trigger a user-visible notification. However, information about how frequently the app called this function is included in the exposure checks under the device's Exposure Notifications settings. |
getExposureInformation()
|
In legacy v1 mode, the Google Play services displays a user-visible notification to the user each time this method is invoked.
This method does not support There are no quotas for this method. |
getExposureWindows()
|
The Because long exposures to one key are split into windows of up to 30 minutes of scans, a given key may lead to several exposure windows if beacon sightings for it spanned more than 30 minutes. The link between them (in other words, the fact that they all correspond to the same key) is lost because those windows are shuffled before being returned and the underlying keys are not exposed. This method has no quota, and calling this method does NOT by itself trigger a user-visible notification. However, information about how frequently the app called this function is included in the exposure checks under the device's Exposure Notifications settings. |
getDailySummaries()
|
The
This function returns a list of
This function accepts a This method has no quota, and calling this method does NOT by itself trigger a user-visible notification. |
getExposureSummary()
Note: We strongly recommend migrating to ExposureWindow mode for enhanced risk calculation functionality.
|
In legacy v1 mode, the
This method does not support There are no quotas for this method. |
getExposureInformation()
Note: We strongly recommend migrating to ExposureWindow mode for enhanced risk calculation functionality.
|
In legacy v1 mode, the Google Play services displays a notification to the user each time this method is invoked.
This method does not support There are no quotas for this method. |
Broadcast receivers
Your app must register a receiver to receive broadcasts of the
ACTION_EXPOSURE_STATE_UPDATED
and ACTION_EXPOSURE_NOT_FOUND
intents.
After provideDiagnosisKeys()
is called, if a match is found, the
ACTION_EXPOSURE_STATE_UPDATED
broadcast is sent; if no match is found, the
ACTION_EXPOSURE_NOT_FOUND
broadcast is sent. Present information to the
user in response to the broadcasts as needed. The following code sample shows
how to do this:
public class ExposureNotificationBroadcastReceiver extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent)
{
String action = intent.getAction();
if (ExposureNotificationClient.ACTION_EXPOSURE_STATE_UPDATED
.equals(action))
{
// Handle exposure found action
}
else if (ExposureNotificationClient.ACTION_EXPOSURE_NOT_FOUND
.equals(action))
{
// Handle exposure not found action
}
else if (ExposureNotificationClient.ACTION_SERVICE_STATE_UPDATED
.equals(action))
{
// Handle service state change (for example, user manually disabled it)
}
}
}
When you declare the receiver, you must add the
com.google.android.gms.nearby.exposurenotification.EXPOSURE_CALLBACK
permission to the receiver declaration in your Android manifest.
The following code snippet shows the entry for your manifest:
<receiver
android:name=".exposurenotification.app.ExposureNotificationBroadcast"
android:permission="com.google.android.gms.nearby.exposurenotification.EXPOSURE_CALLBACK">
<intent-filter>
<action android:name="com.google.android.gms.exposurenotification.ACTION_EXPOSURE_STATE_UPDATED" />
<action android:name="com.google.android.gms.exposurenotification.ACTION_EXPOSURE_NOT_FOUND" />
<action android:name="com.google.android.gms.exposurenotification.ACTION_SERVICE_STATE_UPDATED" />
</intent-filter>
</receiver>
Your app can be be notified of Exposure Notifications state changes when either of the following happens:
- The user modifies the state of the Exposure Notifications system on the Google Settings page.
- The user switches to a different Exposure Notifications app.
To be notified, your app must register a receiver to receive broadcasts from the
ACTION_SERVICE_STATE_UPDATED
intent. The boolean EXTRA_SERVICE_STATE
extra
is included as part of the broadcast, and returns a state indicating whether the
system is enabled or disabled.
For more information about broadcasts, see Broadcasts overview.
TEK validation
Especially when configuring your app to accommodate roaming, it is important to validate incoming TEK keys to avoid corrupted or outdated data. Validating TEKs ensures that the Exposure Notifications system will be able to parse them and that the server will contain only up-to-date keys.
For more information on some of the elements to consider when validating TEKs, see Recommended Temporary Exposure Key validation.
Error handling
To handle the errors of method calls, do the following:
- Add a failure callback to the returned Task using
Task.addOnFailureListener()
. - Cast the returned object to the
APIException
class. - Get the
common.api.Status
using thecommon.api.getStatus()
method, which provides information used to troubleshoot.
The Status
provides:
- Status code: an integer that maps to any of the CommonStatusCodes.
- Resolution:
checks if a status has a resolution using
hasResolution()
and if so, callstartResolutionForResult()
to start the resolution flow. ConnectionResult
: provides further information about the connection result by retrieving the error code usinggetErrorCode()
. You can map this code with any of theExposureNotificationStatusCodes
.
For example, the status might indicate that the user has not opted-in
(startResolutionForResult()
would trigger the consent dialog), the service
isn’t running, or that there's insufficient storage on the device to complete an
operation.
client.start().addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception exception) {
if (exception instanceof ApiException) {
Status status = ((ApiException) exception).getStatus();
if (status.hasResolution()) {
status.startResolutionForResult(activity, REQUEST_START_CODE);
} else {
// Consider handling the different ExposureNotificationStatusCodes
handleStatus(status.getConnectionResult().getErrorCode());
}
}
}
});
In the preceding code, if there is an ApiException
with a possible resolution.
You launch it and handle the onActivityResult()
of the given activity,
as shown in the following code:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case REQUEST_START_CODE:
if (resultCode == Activity.RESULT_OK) {
// resolution solved (for example, the user gave consent)
// retry the action, for example start EN API again.
startClient();
} else {
// resolution rejected or cancelled
}
break;
}
}
In the case that the status was not recoverable, you could consider handling the
connection result error code that can be mapped to the
ExposureNotificationStatusCodes
.
private void handleStatus(int errorCode) {
switch (errorCode) {
case ExposureNotificationStatusCodes.FAILED_RATE_LIMITED:
// The client has been rate-limited for access to this API.
break;
case ExposureNotificationStatusCodes.FAILED_UNAUTHORIZED:
// The client is unauthorized to access the APIs.
break;
case ExposureNotificationStatusCodes.FAILED_SERVICE_DISABLED:
// The functionality was disabled by the user or the phone.
break;
case ExposureNotificationStatusCodes.FAILED_NOT_SUPPORTED:
// The hardware capability of the device is not supported.
break;
case ExposureNotificationStatusCodes.FAILED_BLUETOOTH_DISABLED:
// Bluetooth is disabled.
break;
default:
// more status codes in ExposureNotificationStatusCodes class.
break;
}
}
Force-stop handling
On devices that have version v1.5 or higher of the Exposure Notifications system, the system can automatically start the active Exposure Notifications process by binding to a service when it is force-stopped. While the app remains in the background, this action ensures that the app can execute any periodic tasks or receive broadcast events.
To handle this functionality, rebuild your app against the v1.5 or higher API and ensure that you are rescheduling any periodic tasks when the application process starts.
The system will restart your app every six hours. On many devices, power-saving interventions will prevent your app from fetching and processing keys any more frequently than this.
Handle API timeouts
While the EN API methods all return a result, there might be some exceptional situations in which Google Play services exits unexpectedly.
In these situations, the API may not provide a result via the Tasks instance. While they're rare, it's important to safeguard your code with timeout mechanisms. Some ways you can do this might include:
Using withTimeout(..) when executing Kotlin coroutines.
Using Futures timeout mechanisms.
Reference design
For examples that show how to integrate with the Exposure Notifications API, see the reference design source code hosted on GitHub.
Glossary
- Allowlisted accounts
- We have partnered with public health agencies for purposes of collaboration and testing of Exposure Notifications apps while they are in development. An allowlist grants specific permissions for these Google partners.
- BLE beacons
- Bluetooth Low Energy (BLE) beacons enable Bluetooth-equipped devices to share information when they're within range of each other.
- Diagnosis key
- The Temporary Exposure Key provided by the server that have been confirmed to have a positive diagnosis of COVID-19. It's used by the app to check for exposure.
- Rotating Proximity Identifiers (RPIs)
- A random ID derived from the device's Temporary Exposure Key. The RPI is generated on the device and a new RPI is generated every 10-20 minutes.
- Temporary Exposure Key (TEK)
- A key that's randomly generated once every 24 hours. It remains on the device for up to 14 days. In the event that there's a positive diagnosis of COVID-19, and upon permission granted from the user, these keys are provided to the app.
- Report type (
reportType
) - Classifies the method of COVID-19 diagnosis for a given diagnosis key.
- Transmission Risk
- (Legacy v1 mode)
- In legacy v1 mode, a value defined by your app that specifies how much risk is associated with a given exposure based on a value of importance to your health authority. This enables you to add an additional, non-API-specified value to the computation of total risk.