Skip to content

Commit

Permalink
Merge pull request #182 from DP-3T/develop
Browse files Browse the repository at this point in the history
Release 1.0.1
  • Loading branch information
simonroesch authored Jul 16, 2020
2 parents a59b4b5 + 7541c24 commit 9451af8
Show file tree
Hide file tree
Showing 24 changed files with 552 additions and 200 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog for DP3T-SDK Android

## version 1.0.1 (16.7.2020)

- less frequent error notifications (only once per error while errors persist, wait 5min for GPS/Bluetooth state changes, wait 24h before showing EN API Errors)
- prevent rate limit errors when EN Api returns an error
- do not show http response code 504 error directly (like we already did for 502 and 503)
- fixed serialization issue in combination with core library desugaring

## version 1.0.0 (18.6.2020)

- prevent sync from running when ExposureNotifications disabled
Expand Down
26 changes: 14 additions & 12 deletions EXPOSURE_NOTIFICATION_API_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,46 @@ This document outlines the interaction of the SDK with the [Exposure Notificatio

## Enabling Exposure Notifications

To enable Exposure Notifications for our app we need to call exposureNotificationClient.start(). This will trigger a system popup asking the user to either enable Exposure Notifications on this device or (if another app is active) to switch to our app as active Exposure Notifications app. If this popup needs to be shown to the user, the start() method results in an ApiException that allows us to call startResolutionForResult() to trigger the system popup. The result (user accepts or declines) is then returned as an activity result intent.
To enable Exposure Notifications for our app we need to call `exposureNotificationClient.start()`. This will trigger a system popup asking the user to either enable Exposure Notifications on this device or (if another app is active) to switch to our app as active Exposure Notifications app. If this popup needs to be shown to the user, the `start()` method results in an ApiException that allows us to call `startResolutionForResult()` to trigger the system popup. The result (user accepts or declines) is then returned as an activity result intent.

## Disabling Exposure Notifications

To disable Exposure Notifications for our app we need to call exposureNotificationClient.stop(). This will stop the BLE broadcasting and scanning until in our app (or another Exposure Notifications app) start() is called again.
To disable Exposure Notifications for our app we need to call `exposureNotificationClient.stop()`. This will stop the BLE broadcasting and scanning until in our app (or another Exposure Notifications app) `start()` is called again.

## Check if Exposure Notifications enabled

To check if Exposure Notifications are still enabled for our app (the user could have activated them in another app and thus deactivated them for our app) we need to call exposureNotificationClient.isEnabled().
To check if Exposure Notifications are still enabled for our app (the user could have activated them in another app and thus deactivated them for our app) we need to call `exposureNotificationClient.isEnabled()`.

## Exporting Temporary Exposure Keys

To retrieve the Temporary Exposure Keys (TEKs) we need to call exposureNotificationClient.getTemporaryExposureKeyHistory(). This will result in an ApiException if Exposure Notifications are disabled. If enabled the method results in an ApiException that allows us to call startResolutionForResult() to trigger a system popup asking the user if he wants to share the TEKs of the last 14 days with the app. The result (user accepts or declines in the popup) is then returned as an activity result intent. If the user agrees to share the keys with the app in the popup the next call to getTemporaryExposureKeyHistory() will return the TEKs directly without additional Exception/system popup.
To retrieve the Temporary Exposure Keys (TEKs) we need to call `exposureNotificationClient.getTemporaryExposureKeyHistory()`. This will result in an `ApiException` if Exposure Notifications are disabled. If enabled the method results in an `ApiException` that allows us to call `startResolutionForResult()` to trigger a system popup asking the user if he wants to share the TEKs of the last 14 days with the app. The result (user accepts or declines in the popup) is then returned as an activity result intent. If the user agrees to share the keys with the app in the popup the next call to `getTemporaryExposureKeyHistory()` will return the TEKs directly without an additional Exception or system popup.

The TEK of the current day is never returned by getTemporaryExposureKeyHistory(), but only the keys of the previous 13 days. After the user agreed to share the keys we can call getTemporaryExposureKeyHistory() again on the following day and will then receive the TEK of the day the user agreed to share the keys as well. For this to work, Exposure Notifications must still be active for our app.
The TEK of the current day is never returned by `getTemporaryExposureKeyHistory()`, but only the keys of the previous 13 days. After the user agreed to share the keys we can call `getTemporaryExposureKeyHistory()` again on the following day and will then receive the TEK of the day the user agreed to share the keys as well. For this to work, Exposure Notifications must still be active for our app.

## Detecting Exposure

For a contact to be counted as a possible exposure it must be longer than a certain number of minutes on a certain day. The current implementation of the EN-framework does not expose this information. Our way to overcome this limitation is to pass the published keys for each day individually to the framework.

To check for exposure on a given day (we check the past 10 days) we need to call exposureNotificationClient.provideDiagnosisKeys(). This method has three parameters:
To check for exposure on a given day (we check the past 10 days) we need to call `exposureNotificationClient.provideDiagnosisKeys()`. This method has three parameters:

#### File list
TEKs to check for exposure against must be provided in a [special file format](https://developers.google.com/android/exposure-notifications/exposure-key-file-format). The API would allow to provide multiple files, but we always provide all available keys in a single file.
TEKs to check for exposure against must be provided in a [special file format](https://developers.google.com/android/exposure-notifications/exposure-key-file-format). The API would allow for multiple files being provided, but we always provide all available keys in a single file.

#### Exposure Configuration
The exposure configuration defines the configuration for the Google scoring of exposures. In our case we ignore most of the scoring methods and only provide the thresholds for the duration at attenuation buckets. The thresholds for the attenuation buckets are loaded from our [config server](https://github.com/DP-3T/dp3t-config-backend-ch/blob/master/dpppt-config-backend/src/main/java/org/dpppt/switzerland/backend/sdk/config/ws/model/GAENSDKConfig.java). This allows us to group the duration of a contact with another device into three buckets regarding the measured attenuation values that we then use to detect if the contact was long enough and close ennough.
The exposure configuration defines the configuration for the Google scoring of exposures. In our case we ignore most of the scoring methods and only provide the thresholds for the duration at attenuation buckets. The thresholds for the attenuation buckets are loaded from our [config server](https://github.com/DP-3T/dp3t-config-backend-ch/blob/master/dpppt-config-backend/src/main/java/org/dpppt/switzerland/backend/sdk/config/ws/model/GAENSDKConfig.java). This allows us to group the duration of a contact with another device into three buckets regarding the measured attenuation values that we then use to detect if the contact was long enough and close enough.
To detect an exposure the following formula is used to compute the exposure duration:
```
durationAttenuationLow * factorLow + durationAtttenuationMedium * factorMedium
durationAttenuationLow * factorLow + durationAttenuationMedium * factorMedium
```
If this duration is at least as much as defined in the triggerThreshold a notification is triggered for that day.
If this duration is at least as much as defined in the `triggerThreshold` a notification is triggered for that day.

#### Token
Providing a token allows us to update an exposure check executed previously and only providing additional new TEKs in the file. The previously provided TEKs for the same token are stored internally by the framework and the new exposure result is the total exposure with all provided TEKs in the current and previous calls. When we download TEKs from our backend we receive a token (timestamp) that we can use for the next sync to only download the newly added TEKs. This reduces traffic between the app and the backend.

### Result
The result of the provideDiagnosisKeys() call is provided as a broadcast with action ExposureNotificationClient.ACTION_EXPOSURE_STATE_UPDATED. In the Intent we directly get the ExposureSummary object, that allows us to check if the exposure limit for a notification was reached by checking the minutes of exposure per attenuation window. The duration per window has a maximum of 30min, longer exposures are also returned as 30min of exposure.

The result of the `provideDiagnosisKeys()` call is provided as a broadcast with action `ExposureNotificationClient.ACTION_EXPOSURE_STATE_UPDATED`. In the Intent we directly get the `ExposureSummary` object, that allows us to check if the exposure limit for a notification was reached by checking the minutes of exposure per attenuation window. The duration per window has a maximum of 30 minutes, longer exposures are also returned as 30 minutes of exposure.

### Rate limit
We are only allowed to call provideDiagnosisKeys() 20 times for each UTC day. Because we check for every of the past 10 days individually, this allows us to check for exposure twice per day. These checks happen after 6am and 6pm (swiss time) when the SyncWorker is scheduled the next time or the app is opened. All 10 days are checked individually and if one fails it is retried on the next run. No checks are made between midnight UTC and 6am (swiss time) to prevent exceeding the rate limit per UTC day.

We are only allowed to call `provideDiagnosisKeys()` 20 times per UTC day. Because we check for every of the past 10 days individually, this allows us to check for exposure at most twice per day. These checks happen after 6am and 6pm (swiss time) when the SyncWorker is scheduled the next time or the app is opened. All 10 days are checked individually and if one fails it is retried on the next run. No checks are made between midnight UTC and 6am (swiss time) to prevent exceeding the rate limit per UTC day.
4 changes: 2 additions & 2 deletions dp3t-sdk/sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ android {
defaultConfig {
minSdkVersion 23
targetSdkVersion 29
versionCode 100
versionName "1.0.0"
versionCode 101
versionName "1.0.1"
testInstrumentationRunnerArgument 'androidx.benchmark.suppressErrors', 'EMULATOR,LOW-BATTERY,ACTIVITY-MISSING,DEBUGGABLE,UNLOCKED,UNSUSTAINED-ACTIVITY-MISSING'
testInstrumentationRunner "androidx.benchmark.junit4.AndroidBenchmarkRunner"

Expand Down
1 change: 1 addition & 0 deletions dp3t-sdk/sdk/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
-keep class org.dpppt.android.sdk.models.** { *; }
-keep class org.dpppt.android.sdk.internal.backend.models.** { *; }
-keep class org.dpppt.android.sdk.internal.storage.models.** { *; }

-keep class com.google.crypto.tink.proto.** { *; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import org.dpppt.android.sdk.internal.storage.ExposureDayStorage;
import org.dpppt.android.sdk.models.DayDate;
import org.dpppt.android.sdk.models.ExposureDay;
import org.junit.Before;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
import org.dpppt.android.sdk.DP3T;
import org.dpppt.android.sdk.InfectionStatus;
import org.dpppt.android.sdk.TracingStatus;
import org.dpppt.android.sdk.internal.backend.ProxyConfig;
import org.dpppt.android.sdk.internal.logger.LogLevel;
import org.dpppt.android.sdk.internal.logger.Logger;
import org.dpppt.android.sdk.internal.nearby.GaenStateHelper;
import org.dpppt.android.sdk.internal.nearby.GoogleExposureClient;
import org.dpppt.android.sdk.internal.nearby.TestGoogleExposureClient;
import org.dpppt.android.sdk.internal.util.Json;
Expand Down Expand Up @@ -55,6 +57,8 @@ public void setup() throws IOException {

testGoogleExposureClient = new TestGoogleExposureClient(context);
GoogleExposureClient.wrapTestClient(testGoogleExposureClient);
ProxyConfig.DISABLE_SYSTEM_PROXY = true;
GaenStateHelper.SET_GAEN_AVAILABILITY_AVAILABLE_FOR_TESTS = true;

server = new MockWebServer();
server.start();
Expand Down
5 changes: 4 additions & 1 deletion dp3t-sdk/sdk/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
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.settings.EXPOSURE_NOTIFICATION_SETTINGS" />
</intent-filter>
</receiver>

Expand All @@ -42,6 +41,10 @@
</intent-filter>
</receiver>

<receiver android:name="org.dpppt.android.sdk.internal.TracingErrorsBroadcastReceiver"
android:exported="false">
</receiver>

</application>

</manifest>
24 changes: 13 additions & 11 deletions dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/DP3T.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
import org.dpppt.android.sdk.internal.nearby.GaenStateCache;
import org.dpppt.android.sdk.internal.nearby.GaenStateHelper;
import org.dpppt.android.sdk.internal.nearby.GoogleExposureClient;
import org.dpppt.android.sdk.internal.storage.ErrorNotificationStorage;
import org.dpppt.android.sdk.internal.storage.ExposureDayStorage;
import org.dpppt.android.sdk.internal.storage.models.PendingKey;
import org.dpppt.android.sdk.internal.storage.PendingKeyUploadStorage;
import org.dpppt.android.sdk.models.ApplicationInfo;
import org.dpppt.android.sdk.models.DayDate;
import org.dpppt.android.sdk.models.ExposeeAuthMethod;
Expand Down Expand Up @@ -255,11 +259,7 @@ private static void executeIAmInfected(Activity activity) {
new ResponseCallback<String>() {
@Override
public void onSuccess(String authToken) {
PendingKeyUploadStorage.PendingKey delayedKey =
new PendingKeyUploadStorage.PendingKey(
delayedKeyDate,
authToken,
0);
PendingKey delayedKey = new PendingKey(delayedKeyDate, authToken, 0);
PendingKeyUploadStorage.getInstance(activity).addPendingKey(delayedKey);
appConfigManager.setIAmInfected(true);
pendingIAmInfectedRequest.callback.onSuccess(null);
Expand Down Expand Up @@ -303,10 +303,7 @@ public static void sendFakeInfectedRequest(Context context, ExposeeAuthMethod ex
new ResponseCallback<String>() {
@Override
public void onSuccess(String authToken) {
PendingKeyUploadStorage.PendingKey delayedKey = new PendingKeyUploadStorage.PendingKey(
delayedKeyDate,
authToken,
1);
PendingKey delayedKey = new PendingKey(delayedKeyDate, authToken, 1);
PendingKeyUploadStorage.getInstance(context).addPendingKey(delayedKey);
Logger.d(TAG, "successfully sent fake request");
if (devHistory) {
Expand Down Expand Up @@ -384,8 +381,12 @@ public static String getUserAgent() {
return userAgent;
}

public static void setNetworkErrorGracePeriod(long gracePeriodMillis) {
SyncErrorState.getInstance().setNetworkErrorGracePeriod(gracePeriodMillis);
public static void setSyncErrorGracePeriod(long gracePeriodMillis) {
SyncErrorState.getInstance().setSyncErrorGracePeriod(gracePeriodMillis);
}

public static void setErrorNotificationGracePeriod(long gracePeriodMillis) {
SyncErrorState.getInstance().setErrorNotificationGracePeriod(gracePeriodMillis);
}

public static IntentFilter getUpdateIntentFilter() {
Expand Down Expand Up @@ -414,6 +415,7 @@ public static void clearData(Context context) {
appConfigManager.clearPreferences();
ExposureDayStorage.getInstance(context).clear();
PendingKeyUploadStorage.getInstance(context).clear();
ErrorNotificationStorage.getInstance(context).clear();
Logger.clear();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package org.dpppt.android.sdk;

import android.content.Context;
import androidx.annotation.Keep;
import androidx.annotation.StringRes;

import java.util.Collection;
Expand Down Expand Up @@ -54,6 +55,7 @@ public Collection<ErrorState> getErrors() {
return errors;
}

@Keep
public enum ErrorState {
LOCATION_SERVICE_DISABLED(R.string.dp3t_sdk_service_notification_error_location_service),
BLE_DISABLED(R.string.dp3t_sdk_service_notification_error_bluetooth_disabled),
Expand Down Expand Up @@ -96,6 +98,15 @@ public String getErrorString(Context context) {
}
return text;
}

public static ErrorState tryValueOf(String name) {
for (ErrorState value : values()) {
if (value.name().equals(name)) {
return value;
}
}
return null;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public static synchronized AppConfigManager getInstance(Context context) {
private static final String PREF_CALIBRATION_TEST_DEVICE_NAME = "calibrationTestDeviceName";
private static final String PREF_LAST_LOADED_TIMES = "lastLoadedTimes";
private static final String PREF_LAST_SYNC_CALL_TIMES = "lastExposureClientCalls";
private static final String PREF_LAST_SUCCESSFUL_SYNC_TIMES = "lastSuccessfulSyncTimes";
private static final String PREF_DEV_HISTORY = "devHistory";

private static final String PREF_ATTENUATION_THRESHOLD_LOW = "attenuationThresholdLow";
Expand Down Expand Up @@ -185,13 +186,20 @@ public HashMap<DayDate, Long> getLastSyncCallTimes() {
return convertToDateMap(Json.fromJson(sharedPrefs.getString(PREF_LAST_SYNC_CALL_TIMES, "{}"), StringLongMap.class));
}

public HashMap<DayDate, Long> getLastSuccessfulSyncTimes() {
return convertToDateMap(Json.fromJson(sharedPrefs.getString(PREF_LAST_SUCCESSFUL_SYNC_TIMES, "{}"), StringLongMap.class));
}

public void setLastLoadedTimes(HashMap<DayDate, Long> lastLoadedTimes) {
sharedPrefs.edit().putString(PREF_LAST_LOADED_TIMES, Json.toJson(convertFromDateMap(lastLoadedTimes))).apply();
}

public void setLastSyncCallTimes(HashMap<DayDate, Long> lastExposureClientCalls) {
sharedPrefs.edit().putString(PREF_LAST_SYNC_CALL_TIMES, Json.toJson(convertFromDateMap(lastExposureClientCalls)))
.apply();
sharedPrefs.edit().putString(PREF_LAST_SYNC_CALL_TIMES, Json.toJson(convertFromDateMap(lastExposureClientCalls))).apply();
}

public void setLastSuccessfulSyncTimes(HashMap<DayDate, Long> lastSuccessfulSyncTimes) {
sharedPrefs.edit().putString(PREF_LAST_SUCCESSFUL_SYNC_TIMES, Json.toJson(convertFromDateMap(lastSuccessfulSyncTimes))).apply();
}

public void setDevHistory(boolean devHistory) {
Expand Down
Loading

0 comments on commit 9451af8

Please sign in to comment.