diff --git a/CALIBRATION_APP_USAGE.md b/CALIBRATION_APP_USAGE.md new file mode 100644 index 00000000..f7b65495 --- /dev/null +++ b/CALIBRATION_APP_USAGE.md @@ -0,0 +1,72 @@ +# How to use the calibration app for experiments + +## Setup + +Make sure that the calibration app is the only installed ExposureNotifications app (check Settings > Google > COVID-19 Exposure Notifications), otherwise clearing the exposure history is not possible over adb. + +### Make sure your device supports ExposureNotification v1.5 +Go to Settings > Google > COVID-19 exposure notifications and scroll to the bottom of the screen. Verify that the version number starts with 15... (Be aware that version 15xxxxxxxxx is newer than version 20xxxxxxx.) + +Alternatively you can use the following adb command: +``` +adb shell dumpsys activity provider com.google.android.gms.chimera.container.GmsModuleProvider | grep 'nearby_en' +``` +This will show you the EN version, but here the leading 15 for v1.5 is not printed. Known v1.5 versions are: v203004001, v202902002 + +### Enable ExposureNotification Debug Mode +Go to Settings > Google > COVID-19 exposure notifications > Debug mode and make sure that the following options are enabled: +- Bypass app signature check +- Return all TEKs immediately +The option "Enable diagnosis key file signature check" should not be enabled. + +### Reset app and exposure history +Run the following adb command to clear app and exposure history: +``` +adb shell pm clear org.dpppt.android.calibration +``` +Alternatively you can go to Settings > Apps > Calibration App and select "Clear data". + +### Configure app +Set experiment name and device name with the following adb command (replace expName and devName): +``` +adb shell am broadcast -a org.dpppt.android.calibration.adb --es experimentName "expName" --es deviceName "devName" -n org.dpppt.android.calibration/.handshakes.ADBBroadcastReceiver +``` +Alternatively you can also open the app and enter experiment and device name in the parameters tab (second icon at the bottom). + +Now you have a clean setup of the app where device and experiment name are already set. + + +## Run experiment +To start an experiment open the app and press the red "START TRACING" button. An additional popup will show up where you have to agree to enable Exposure Notifications. + +At the end of the experiment go to the parameters tab (second icon at the bottom) and press "UPLOAD KEYS FOR EXPERIMENT". Make sure that the parameters from the setup phase are set correctly. + +To stop tracing and prevent any side-effects go back to the controls tab (first icon at the bottom) and press "STOP TRACING". + +## Execute matching + +Make sure tracing is enabled on the device for the matching to work (this is a restriction of the Google ExposureNotification framework). To do this, go to the controls tab (first icon at the bottom) of the app and press "START TRACING" if not already active. + +After all devices have uploaded their keys you have two options to execute the matching of a certain experiment: +### Matching with ADB +Run the following command to start the matching over adb (replace expName with the name of your experiment) +``` +adb shell am broadcast -a org.dpppt.android.calibration.adb --es runMatching "expName" -n org.dpppt.android.calibration/.handshakes.ADBBroadcastReceiver +``` +Check logcat to see when the matching finished (search for MatchingWorker), you will see the message "matching executed and uploaded successfully!". + +### Matching from the app +Go to the handshakes tab (third icon at the bottom) of the app and you will see a list of all available experiments. Click an experiment to run the matching. +The result will be displayed in the app but also uploaded to the server. + +### Check matching results +The matching result will be automatically uploaded as JSON to the following URL: +https://dp3tdemo.blob.core.windows.net/fileupload/result_experiment_{EXPERIMENT_NAME}_{DATE}_device_{DEVICE_NAME}.json +Example: +https://dp3tdemo.blob.core.windows.net/fileupload/result_experiment_1_2020-07-19_device_DD.json + +## Good to know + +- You can only execute one experiment and one (!) matching after a clean install. All subsequent experiments / matching runs will not work or lead to wrong results. + +- Battery optimization does not have to be deactivated for experiments, this is only needed to guarantee periodic backend syncs in the background, which is irrelevant for experiments. The error notification can thus be ignored. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e51e1db..1f49b1ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog for DP3T-SDK Android +## version 2.0.0 (28.10.2020) + +- updated to play-services-nearby-exposurenotification-1.7.1-eap.aar, make sure to update this in your project as well! +- use exposureWindows to compute attenuationDurations +- exposureDays now returns all exposure days, not only the last one as in previous versions +- updated defaults for attenuationBucketThresholds, new values are 55 and 63 +- add Experiment-Mode to calibration app to simplify experiments with multiple devices and the new ExposureWindows-API (see CALIBRATION_APP_USAGE.md for details) +- expose EN-Module version, this can be used to extend the userAgent to be able to handle potential bugs in future EN versions from the backend +- Version 2.0.0 of the SDK will require EN module version >= 1.6, if run on older versions a notification will be generated asking the user to update Google Play Services +- add config option DP3T.setNumberOfDaysToConsiderForExposure() to define how many days after the exposure an exposure should be considered +- add config option DP3T.setNumberOfDaysToKeepExposedDays() to define how many days after an exposure is reported, this should be kept + ## version 1.0.5 (24.9.2020) - support location less scanning on Android 11 diff --git a/EXPOSURE_NOTIFICATION_API_USAGE.md b/EXPOSURE_NOTIFICATION_API_USAGE.md index d06dae9f..9188bc6b 100644 --- a/EXPOSURE_NOTIFICATION_API_USAGE.md +++ b/EXPOSURE_NOTIFICATION_API_USAGE.md @@ -1,5 +1,5 @@ # ExposureNotification API usage -This document outlines the interaction of the SDK with the [Exposure Notification](https://www.google.com/covid19/exposurenotifications/) Framework by Google. +This document outlines the interaction of the SDK with the [Exposure Notification](https://www.google.com/covid19/exposurenotifications/) Framework v1.5 by Google. ## Enabling Exposure Notifications @@ -17,32 +17,39 @@ To check if Exposure Notifications are still enabled for our app (the user could 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. +There are two possibilities how the TEK of the current day can be retrieved. Which case is currently active is decided by a configuration of Google. +### Same day TEK release with shortened rolling period +In this case, the TEK of the current day and the previous 13 days are returned by `getTemporaryExposureKeyHistory()`. The TEK of the current day has a rolling period that is shorter than 24h, so this key is no longer valid after it was returned. If the device continues to have exposure notifications active a new TEK will be used on the same day. Therefore, it is possible that for the same date multiple TEKs are returned by `getTemporaryExposureKeyHistory()`, if the user allowed to export the keys already once or more in the previous 14 days. + +### Next day TEK release +In this case, the TEK of the current day is not 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. +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 directly, but as of version 1.5 of the exposure notification framework, we can use the new exposure windows feature to calculate the information ourselves. -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 we need to call `exposureNotificationClient.provideDiagnosisKeys()`. This method has only one parameter and takes a file list containing the TEKs. -#### 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 for multiple files being provided, but we always provide all available keys in a single file. +These 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 enough. -To detect an exposure the following formula is used to compute the exposure duration: -``` -durationAttenuationLow * factorLow + durationAttenuationMedium * factorMedium -``` -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 30 minutes, longer exposures are also returned as 30 minutes of exposure. +The result of the `provideDiagnosisKeys()` call is provided as a broadcast with action `ExposureNotificationClient.ACTION_EXPOSURE_STATE_UPDATED`. After this we can call [`getExposureWindows()`](https://developers.google.com/android/exposure-notifications/exposure-notifications-api#exposurewindow) to get a list of `ExposureWindows` describing our current state of exposure. + + +#### Calculation of exposure from ExposureWindows +A `ExposureWindow` is a set of Bluetooth scan events from observed beacons within a timespan. A window contains multiple `ScanInstance` which are aggregations of attenuation of beacons during a scan. + +By grouping the ExposureWindows by day and then adding up all `secondsSinceLastScan` where `typicalAttenuationDb` lies between our defined attenuation thresholds we can compose three 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). + +To detect an exposure the following formula is used to compute the exposure duration: +``` +durationAttenuationLow * factorLow + durationAtttenuationMedium * factorMedium +``` ### Rate limit -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. +We are only allowed to call `provideDiagnosisKeys()` 6 times per UTC day. Therefore, after an attempted call to `provideDiagnosisKeys()` we always wait 4h before doing the next call to guarantee to stay within the rate limit. diff --git a/README.md b/README.md index fc6f1548..1330f7fd 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ DP-3T is a free-standing effort started at EPFL and ETHZ that produced this prot ## Introduction This is the implementation of the DP-3T protocol using the [Exposure Notification](https://www.google.com/covid19/exposurenotifications/) Framework of Apple/Google. Only approved government public health authorities can access the APIs. Therefore, using this SDK will result in an API error unless either your account is whitelisted as test account or your app is approved by Google and signed with the production certificate. +As of version 2.0 of this SDK we use features added in v1.5 of the Google framework and for iOS features that were added in v2.0 of the Apple framework. Make sure to also use at least version 2.0 of the [dp3t-sdk-backend](https://github.com/DP-3T/dp3t-sdk-backend) to be compatible with the DP3T Android SDK 2.0. See [EXPOSURE_NOTIFICATION_API_USAGE.md](EXPOSURE_NOTIFICATION_API_USAGE.md) for a detailed description of how we use the Google EN Api. + Our prestandard solution that is not using the Apple/Google framework can be found under the [tag prestandard](https://github.com/DP-3T/dp3t-sdk-android/tree/prestandard). ## Repositories @@ -27,6 +29,8 @@ The full set of documents for DP3T is at https://github.com/DP-3T/documents. Ple ## Calibration App Included in this repository is a Calibration App that can run, debug and test the SDK directly without implementing it in a new app first. It collects additional data and stores it locally into a database to allow for tests with phones from different vendors. Various parameters of the SDK are exposed and can be changed at runtime. Additionally it provides an overview of how to use the SDK. +See [CALIBRATION_APP_USAGE.md ](CALIBRATION_APP_USAGE.md) for more information on how to use the calibration app. +

diff --git a/calibration-app/app/build.gradle b/calibration-app/app/build.gradle index f89dcb6a..fee7e6ae 100644 --- a/calibration-app/app/build.gradle +++ b/calibration-app/app/build.gradle @@ -30,8 +30,8 @@ android { applicationId "org.dpppt.android.calibration" minSdkVersion 23 targetSdkVersion 30 - versionCode 2 - versionName "0.2" + versionCode 3 + versionName "0.3 (EN1.5)" missingDimensionStrategy 'version', 'calibration' diff --git a/calibration-app/app/src/main/AndroidManifest.xml b/calibration-app/app/src/main/AndroidManifest.xml index 2385991f..ad13ce7b 100644 --- a/calibration-app/app/src/main/AndroidManifest.xml +++ b/calibration-app/app/src/main/AndroidManifest.xml @@ -28,6 +28,14 @@ + + + + + + \ No newline at end of file diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/MainActivity.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/MainActivity.java index 11540e2c..e9ddbae6 100644 --- a/calibration-app/app/src/main/java/org/dpppt/android/calibration/MainActivity.java +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/MainActivity.java @@ -13,6 +13,7 @@ import android.os.Bundle; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; import com.google.android.material.bottomnavigation.BottomNavigationView; @@ -74,6 +75,9 @@ protected void onActivityResult(int requestCode, int resultCode, @Nullable Inten boolean handled = DP3T.onActivityResult(this, requestCode, resultCode, data); if (!handled) { + for (Fragment fragment : getSupportFragmentManager().getFragments()) { + fragment.onActivityResult(requestCode, resultCode, data); + } super.onActivityResult(requestCode, resultCode, data); } } diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/MainApplication.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/MainApplication.java index 12dc82e3..241958f2 100644 --- a/calibration-app/app/src/main/java/org/dpppt/android/calibration/MainApplication.java +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/MainApplication.java @@ -48,7 +48,7 @@ public static void initDP3T(Context context) { "RZ0FFdkxXZHVFWThqcnA4aWNSNEpVSlJaU0JkOFh2UgphR2FLeUg2VlFnTXV2Zk1JcmxrNk92QmtKeH" + "dhbUdNRnFWYW9zOW11di9rWGhZdjF1a1p1R2RjREJBPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t"); DP3T.init(context, - new ApplicationInfo("org.dpppt.demo", BASE_URL, BASE_URL), + new ApplicationInfo(BASE_URL, BASE_URL), signaturePublicKey); if (!BuildConfig.DEBUG) { @@ -58,12 +58,13 @@ public static void initDP3T(Context context) { DP3T.setCertificatePinner(certificatePinner); } - String userAgent = BuildConfig.APPLICATION_ID + ";" + - BuildConfig.VERSION_NAME + ";" + - BuildConfig.VERSION_CODE + ";" + - "Android;" + - Build.VERSION.SDK_INT; - DP3T.setUserAgent(userAgent); + DP3T.setUserAgent(() -> + BuildConfig.APPLICATION_ID + ";" + + BuildConfig.VERSION_NAME + ";" + + BuildConfig.VERSION_CODE + ";" + + "Android;" + + Build.VERSION.SDK_INT + ); } @Override diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/controls/ControlsFragment.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/controls/ControlsFragment.java index 41261b64..6feac6aa 100644 --- a/calibration-app/app/src/main/java/org/dpppt/android/calibration/controls/ControlsFragment.java +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/controls/ControlsFragment.java @@ -31,7 +31,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -49,7 +48,6 @@ import org.dpppt.android.calibration.MainApplication; import org.dpppt.android.calibration.R; -import org.dpppt.android.calibration.handshakes.BackendCalibrationReportRepository; import org.dpppt.android.calibration.util.DialogUtil; import org.dpppt.android.calibration.util.RequirementsUtil; import org.dpppt.android.sdk.DP3T; @@ -58,12 +56,8 @@ import org.dpppt.android.sdk.InfectionStatus; import org.dpppt.android.sdk.TracingStatus; import org.dpppt.android.sdk.backend.ResponseCallback; -import org.dpppt.android.sdk.internal.AppConfigManager; -import org.dpppt.android.sdk.internal.backend.models.GaenRequest; import org.dpppt.android.sdk.internal.export.FileUploadRepository; -import org.dpppt.android.sdk.internal.nearby.GoogleExposureClient; import org.dpppt.android.sdk.models.ExposeeAuthMethodJson; -import org.dpppt.android.sdk.util.DateUtil; import retrofit2.Call; import retrofit2.Callback; @@ -198,14 +192,12 @@ private void setupUi(View view) { setExportDbLoadingViewVisible(true); }); - EditText deanonymizationDeviceId = view.findViewById(R.id.deanonymization_device_id); Button uploadDB = view.findViewById(R.id.home_button_upload_db); uploadDB.setOnClickListener(v -> { - String deviceId = deanonymizationDeviceId.getText().toString(); - DP3TCalibrationHelper.setCalibrationTestDeviceName(getContext(), deviceId); setUploadDbLoadingViewVisible(true); new FileUploadRepository() - .uploadDatabase(requireContext(), AppConfigManager.getInstance(getContext()).getCalibrationTestDeviceName(), + .uploadDatabase(requireContext(), + DP3TCalibrationHelper.getInstance(getContext()).getCalibrationTestDeviceName(), new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { @@ -221,34 +213,6 @@ public void onFailure(@NonNull Call call, @NonNull Throwable t) { } }); }); - - Button deanonymizationButton = view.findViewById(R.id.deanonymization_key_upload_button); - deanonymizationButton.setOnClickListener(v -> { - String deviceId = deanonymizationDeviceId.getText().toString(); - DP3TCalibrationHelper.setCalibrationTestDeviceName(getContext(), deviceId); - GoogleExposureClient.getInstance(getContext()) - .getTemporaryExposureKeyHistory(getActivity(), 123, temporaryExposureKeys -> { - GaenRequest exposeeListRequest = - new GaenRequest(temporaryExposureKeys, DateUtil.getCurrentRollingStartNumber()); - new BackendCalibrationReportRepository(requireContext()) - .addGaenExposee(exposeeListRequest, deviceId, new ResponseCallback() { - @Override - public void onSuccess(Void response) { - Toast.makeText(getContext(), "Uploaded keys!", Toast.LENGTH_LONG).show(); - } - - @Override - public void onError(Throwable throwable) { - Toast.makeText(getContext(), throwable.getLocalizedMessage(), Toast.LENGTH_LONG).show(); - } - }); - }, e -> { - Toast.makeText(getContext(), e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); - }); - }); - if (DP3TCalibrationHelper.getCalibrationTestDeviceName(getContext()) != null) { - deanonymizationDeviceId.setText(DP3TCalibrationHelper.getCalibrationTestDeviceName(getContext())); - } } private void checkPermissionRequirements() { @@ -303,12 +267,12 @@ private void updateSdkStatus() { TextView statusText = view.findViewById(R.id.home_status_text); statusText.setText(formatStatusString(status)); - Button buttonStartStopTracking = view.findViewById(R.id.home_button_start_stop_tracking); + Button buttonStartStopTracing = view.findViewById(R.id.home_button_start_stop_tracing); boolean isRunning = status.isTracingEnabled(); - buttonStartStopTracking.setSelected(isRunning); - buttonStartStopTracking.setText(getString(isRunning ? R.string.button_tracking_stop - : R.string.button_tracking_start)); - buttonStartStopTracking.setOnClickListener(v -> { + buttonStartStopTracing.setSelected(isRunning); + buttonStartStopTracing.setText(getString(isRunning ? R.string.button_tracing_stop + : R.string.button_tracing_start)); + buttonStartStopTracing.setOnClickListener(v -> { if (isRunning) { DP3T.stop(v.getContext()); } else { @@ -346,15 +310,12 @@ private void updateSdkStatus() { v -> { DP3T.sendFakeInfectedRequest(getContext(), null, null, null); }); - - EditText deanonymizationDeviceId = view.findViewById(R.id.deanonymization_device_id); - deanonymizationDeviceId.setText(DP3TCalibrationHelper.getCalibrationTestDeviceName(getContext())); } private SpannableString formatStatusString(TracingStatus status) { SpannableStringBuilder builder = new SpannableStringBuilder(); - boolean isTracking = status.isTracingEnabled(); - builder.append(getString(isTracking ? R.string.status_tracking_active : R.string.status_tracking_inactive)).append("\n") + boolean isTracing = status.isTracingEnabled(); + builder.append(getString(isTracing ? R.string.status_tracing_active : R.string.status_tracing_inactive)).append("\n") .setSpan(new StyleSpan(Typeface.BOLD), 0, builder.length() - 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); builder.append(getString(R.string.status_advertising, status.isTracingEnabled())).append("\n") .append(getString(R.string.status_receiving, status.isTracingEnabled())).append("\n"); @@ -366,6 +327,8 @@ private SpannableString formatStatusString(TracingStatus status) { .append(getString(R.string.status_self_infected, status.getInfectionStatus() == InfectionStatus.INFECTED)) .append("\n") .append(getString(R.string.status_been_exposed, status.getInfectionStatus() == InfectionStatus.EXPOSED)) + .append("\n") + .append("EN-Version: " + DP3T.getENModuleVersion(getContext())) .append("\n"); Collection errors = status.getErrors(); diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/ADBBroadcastReceiver.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/ADBBroadcastReceiver.java new file mode 100644 index 00000000..43870a46 --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/ADBBroadcastReceiver.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ +package org.dpppt.android.calibration.handshakes; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.dpppt.android.sdk.DP3TCalibrationHelper; + +public class ADBBroadcastReceiver extends BroadcastReceiver { + +/* +Can be used with the following cmd: +adb shell am broadcast -a org.dpppt.android.calibration.adb --es experimentName "expName" --es deviceName "devName" + -n org.dpppt.android.calibration/.handshakes.ADBBroadcastReceiver + +or + +adb shell am broadcast -a org.dpppt.android.calibration.adb --es runMatching "expName" + -n org.dpppt.android.calibration/.handshakes.ADBBroadcastReceiver +*/ + + private static final String EXTRA_SET_DEVICE_NAME = "deviceName"; + private static final String EXTRA_SET_EXPERIMENT_NAME = "experimentName"; + private static final String EXTRA_RUN_MATCHING = "runMatching"; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.hasExtra(EXTRA_SET_DEVICE_NAME)) { + DP3TCalibrationHelper.getInstance(context).setCalibrationTestDeviceName(intent.getStringExtra(EXTRA_SET_DEVICE_NAME)); + } + if (intent.hasExtra(EXTRA_SET_EXPERIMENT_NAME)) { + DP3TCalibrationHelper.getInstance(context).setExperimentName(intent.getStringExtra(EXTRA_SET_EXPERIMENT_NAME)); + } + if (intent.hasExtra(EXTRA_RUN_MATCHING)) { + MatchingWorker.startMatchingWorker(context, intent.getStringExtra(EXTRA_RUN_MATCHING)); + } + } + +} diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/Experiment.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/Experiment.java new file mode 100644 index 00000000..d3952a4b --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/Experiment.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ +package org.dpppt.android.calibration.handshakes; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class Experiment { + + String name; + List devices; + + public Experiment(String name) { + this.name = name; + this.devices = new ArrayList<>(); + } + + public String getName() { + return name; + } + + public List getDevices() { + return devices; + } + + public static class Device { + String name; + File file; + + public Device(String name, File file) { + this.name = name; + this.file = file; + } + + public String getName() { + return name; + } + + public File getFile() { + return file; + } + + } + +} diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/HandshakesFragment.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/HandshakesFragment.java index 89ebda61..00bf7b23 100644 --- a/calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/HandshakesFragment.java +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/HandshakesFragment.java @@ -20,18 +20,36 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileOutputStream; +import java.io.*; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; +import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration; +import com.google.android.gms.nearby.exposurenotification.ExposureSummary; +import com.google.android.gms.nearby.exposurenotification.ExposureWindow; +import com.google.android.gms.nearby.exposurenotification.ScanInstance; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializer; + import org.dpppt.android.calibration.R; +import org.dpppt.android.sdk.DP3TCalibrationHelper; +import org.dpppt.android.sdk.internal.AppConfigManager; +import org.dpppt.android.sdk.internal.backend.StatusCodeException; +import org.dpppt.android.sdk.internal.export.FileUploadRepository; import org.dpppt.android.sdk.internal.nearby.GoogleExposureClient; import org.dpppt.android.sdk.models.DayDate; import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Response; public class HandshakesFragment extends Fragment { @@ -67,60 +85,233 @@ private void load() { layout.removeAllViews(); new Thread(() -> { try { - GoogleExposureClient googleExposureClient = GoogleExposureClient.getInstance(context); - long currentTime = System.currentTimeMillis(); - long batchReleaseTime = new DayDate().addDays(1).getStartOfDayTimestamp(); - BackendUserBucketRepository backendBucketRepository = new BackendUserBucketRepository(context); - ResponseBody result = backendBucketRepository.getGaenExposees(batchReleaseTime); - - ZipInputStream zis = new ZipInputStream(result.byteStream()); - ZipEntry zipEntry; - while ((zipEntry = zis.getNextEntry()) != null) { - File file = new File(context.getCacheDir(), batchReleaseTime + "_" + zipEntry.getName()); - BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); - byte[] bytesIn = new byte[1024]; - int read = 0; - while ((read = zis.read(bytesIn)) != -1) { - bos.write(bytesIn, 0, read); - } - bos.close(); - zis.closeEntry(); + HashMap experiments = getExperiments(context); - String token = zipEntry.getName() + " " + currentTime + "_" + - googleExposureClient.getExposureConfiguration().toString().hashCode(); - - getView().post(() -> { + getView().post(() -> { + for (Experiment experiment : experiments.values()) { View view = getLayoutInflater().inflate(R.layout.item_handshake, layout, false); - ((TextView) view.findViewById(R.id.device_name)).setText(token.substring(0, token.indexOf(' '))); + ((TextView) view.findViewById(R.id.device_name)).setText("Experiment: " + experiment.name); + TextView textView = view.findViewById(R.id.device_info); + StringBuilder devices = new StringBuilder(); + String delimiter = ""; + for (Experiment.Device device : experiment.getDevices()) { + devices.append(delimiter); + devices.append(device.getName()); + delimiter = ", "; + } + textView.setText("Devices: " + devices.toString()); layout.addView(view); view.setOnClickListener(v -> { - TextView textView = view.findViewById(R.id.device_info); + v.setOnClickListener(null); textView.setText("Loading..."); new Thread(() -> { - try { - ArrayList fileList = new ArrayList<>(); - fileList.add(file); - googleExposureClient.provideDiagnosisKeys(fileList, token); - Thread.sleep(2000); - googleExposureClient.getExposureSummary(token) - .addOnSuccessListener(exposureSummary -> - view.post(() -> { - textView.setText(exposureSummary.toString()); - })); - } catch (Exception e) { - e.printStackTrace(); - view.post(() -> { - textView.setText("Exception: " + e.getLocalizedMessage()); - }); - } + executeAndUploadMatching(context, experiment, new Callback() { + @Override + public void onResult(HashMap result) { + view.post(() -> { + textView.setText(GSON.toJson(result)); + }); + } + + @Override + public void onFailure(Throwable t) { + view.post(() -> { + textView.setText("Exception: " + t.getMessage()); + }); + } + }); }).start(); }); - }); - } + } + }); } catch (Exception e) { e.printStackTrace(); } }).start(); } + public static HashMap getExperiments(Context context) throws IOException, StatusCodeException { + long batchReleaseTime = new DayDate().addDays(1).getStartOfDayTimestamp(); + BackendUserBucketRepository backendBucketRepository = new BackendUserBucketRepository(context); + ResponseBody responseBody = backendBucketRepository.getGaenExposees(batchReleaseTime); + + HashMap experiments = new HashMap<>(); + + ZipInputStream zis = new ZipInputStream(responseBody.byteStream()); + ZipEntry zipEntry; + while ((zipEntry = zis.getNextEntry()) != null) { + String name = zipEntry.getName(); + Experiment experiment; + String deviceName = "unknown"; + Matcher matcher = Pattern.compile("key_export_experiment_([a-zA-Z0-9]+)_(.+)").matcher(name); + if (matcher.find()) { + String experimentName = matcher.group(1); + deviceName = matcher.group(2); + experiment = experiments.get(experimentName); + if (experiment == null) { + experiment = new Experiment(experimentName); + experiments.put(experimentName, experiment); + } + } else { + deviceName = name.substring(11); + experiment = new Experiment("SingleDevice: " + deviceName); + experiments.put(experiment.getName(), experiment); + } + + File file = new File(context.getCacheDir(), batchReleaseTime + "_" + name); + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); + byte[] bytesIn = new byte[1024]; + int read = 0; + while ((read = zis.read(bytesIn)) != -1) { + bos.write(bytesIn, 0, read); + } + bos.close(); + zis.closeEntry(); + + experiment.devices.add(new Experiment.Device(deviceName, file)); + } + return experiments; + } + + public static void executeAndUploadMatching(Context context, Experiment experiment, Callback callback) { + File file = new File(context.getFilesDir(), + "result_experiment_" + experiment.name + "_" + new DayDate().formatAsString() + + "_device_" + + DP3TCalibrationHelper.getInstance(context).getCalibrationTestDeviceName() + + ".json"); + if (file.exists()) { + callback.onFailure(new RuntimeException( + "This matching has already been computed, computing it again would not lead to meaningfull results.")); + return; + } + AppConfigManager appConfigManager = AppConfigManager.getInstance(context); + GoogleExposureClient googleExposureClient = GoogleExposureClient.getInstance(context); + HashMap resultMap = new HashMap<>(); + List oldExposureWindows = null; + try { + oldExposureWindows = googleExposureClient.getExposureWindows(); + } catch (Exception e) { + e.printStackTrace(); + } + for (Experiment.Device device : experiment.devices) { + try { + ArrayList fileList = new ArrayList<>(); + fileList.add(device.file); + googleExposureClient.provideDiagnosisKeys(fileList); + String token = experiment.name + "_" + device.name + "_" + new DayDate().formatAsString(); + //noinspection deprecation + googleExposureClient.provideDiagnosisKeys(fileList, + new ExposureConfiguration.ExposureConfigurationBuilder() + .setDurationAtAttenuationThresholds( + appConfigManager.getAttenuationThresholdLow(), + appConfigManager.getAttenuationThresholdMedium()) + .build(), + token); + Thread.sleep(2000); + List newExposureWindows = googleExposureClient.getExposureWindows(); + Iterator iterator = newExposureWindows.iterator(); + while (iterator.hasNext()) { + if (oldExposureWindows.contains(iterator.next())) { + iterator.remove(); + } + } + oldExposureWindows.addAll(newExposureWindows); + ExposureResult result = new ExposureResult(); + result.deviceCalibrationConfidence = googleExposureClient.getCalibrationConfidence(); + result.exposureWindows = newExposureWindows; + //noinspection deprecation + result.exposureSummary = googleExposureClient.getExposureSummary(token); + resultMap.put(device.getName(), result); + } catch (Exception e) { + e.printStackTrace(); + callback.onFailure(e); + } + } + try { + new BufferedWriter(new FileWriter(file)).append(GSON.toJson(resultMap)).close(); + } catch (IOException e) { + e.printStackTrace(); + } + new FileUploadRepository().uploadFile(file, new retrofit2.Callback() { + @Override + public void onResponse(Call call, Response response) { + callback.onResult(resultMap); + } + + @Override + public void onFailure(Call call, Throwable t) { + callback.onFailure(t); + } + }); + } + + + private static JsonSerializer exposureWindowJsonSerializer = (src, typeOfSrc, context) -> { + JsonObject jsonObject = new JsonObject(); + + jsonObject.addProperty("dateMillisSinceEpoch", src.getDateMillisSinceEpoch()); + jsonObject.addProperty("reportType", src.getReportType()); + jsonObject.addProperty("infectiousness", src.getInfectiousness()); + jsonObject.addProperty("calibrationConfidence", src.getCalibrationConfidence()); + jsonObject.add("scanInstances", context.serialize(src.getScanInstances())); + + return jsonObject; + }; + + private static JsonSerializer scanInstanceJsonSerializer = (src, typeOfSrc, context) -> { + JsonObject jsonObject = new JsonObject(); + + jsonObject.addProperty("minAttenuationDb", src.getMinAttenuationDb()); + jsonObject.addProperty("typicalAttenuationDb", src.getTypicalAttenuationDb()); + jsonObject.addProperty("secondsSinceLastScan", src.getSecondsSinceLastScan()); + + return jsonObject; + }; + + private static JsonSerializer exposureSummaryJsonSerializer = (src, typeOfSrc, context) -> { + JsonObject jsonObject = new JsonObject(); + + jsonObject.addProperty("daysSinceLastExposure", src.getDaysSinceLastExposure()); + jsonObject.addProperty("matchedKeyCount", src.getMatchedKeyCount()); + jsonObject.addProperty("maximumRiskScore", src.getMaximumRiskScore()); + jsonObject.addProperty("summationRiskScore", src.getSummationRiskScore()); + jsonObject.add("attenuationDurationsInMinutes", context.serialize(src.getAttenuationDurationsInMinutes())); + + return jsonObject; + }; + + private static JsonSerializer exposureResultJsonSerializer = (src, typeOfSrc, context) -> { + JsonObject jsonObject = new JsonObject(); + + jsonObject.addProperty("deviceCalibrationConfidence", src.deviceCalibrationConfidence); + jsonObject.add("exposureSummary", context.serialize(src.exposureSummary)); + jsonObject.add("exposureWindows", context.serialize(src.exposureWindows)); + + return jsonObject; + }; + + private static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(ExposureWindow.class, exposureWindowJsonSerializer) + .registerTypeAdapter(ScanInstance.class, scanInstanceJsonSerializer) + .registerTypeAdapter(ExposureSummary.class, exposureSummaryJsonSerializer) + .registerTypeAdapter(ExposureResult.class, exposureResultJsonSerializer) + .create(); + + + public static class ExposureResult { + + Integer deviceCalibrationConfidence; + List exposureWindows; + ExposureSummary exposureSummary; + + } + + + public interface Callback { + void onResult(HashMap result); + + void onFailure(Throwable t); + + } + } diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/MatchingWorker.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/MatchingWorker.java new file mode 100644 index 00000000..c1a160b7 --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/MatchingWorker.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ +package org.dpppt.android.calibration.handshakes; + + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.work.*; + +import java.util.HashMap; + +import org.dpppt.android.sdk.internal.logger.Logger; + +public class MatchingWorker extends Worker { + + private static final String TAG = "MatchingWorker"; + private static final String ARG_EXPERIMENT_NAME = "argExperimentName"; + + public static void startMatchingWorker(Context context, String experimentName) { + Constraints constraints = new Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build(); + + WorkManager workManager = WorkManager.getInstance(context); + WorkRequest myWorkRequest = new OneTimeWorkRequest.Builder(MatchingWorker.class) + .setInputData(new Data.Builder().putString(ARG_EXPERIMENT_NAME, experimentName).build()) + .setConstraints(constraints) + .build(); + workManager.enqueue(myWorkRequest); + + Logger.d(TAG, "scheduled SyncWorker"); + } + + public MatchingWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public ListenableWorker.Result doWork() { + Logger.d(TAG, "start MatchingWorker"); + Context context = getApplicationContext(); + try { + Experiment experiment = HandshakesFragment.getExperiments(context).get(getInputData().getString(ARG_EXPERIMENT_NAME)); + if (experiment == null) { + Logger.e(TAG, "experiment not found!"); + } else { + HandshakesFragment.executeAndUploadMatching(context, experiment, new HandshakesFragment.Callback() { + @Override + public void onResult(HashMap result) { + Logger.i(TAG, "matching executed and uploaded successfully!"); + } + + @Override + public void onFailure(Throwable t) { + Logger.e(TAG, "matching failed!", t); + } + }); + } + } catch (Exception e) { + e.printStackTrace(); + } + return ListenableWorker.Result.success(); + } + +} diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/parameters/ParametersFragment.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/parameters/ParametersFragment.java index 9c3f6cec..46a2f133 100644 --- a/calibration-app/app/src/main/java/org/dpppt/android/calibration/parameters/ParametersFragment.java +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/parameters/ParametersFragment.java @@ -10,35 +10,41 @@ package org.dpppt.android.calibration.parameters; import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; +import android.widget.Button; import android.widget.EditText; -import android.widget.SeekBar; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatSeekBar; import androidx.fragment.app.Fragment; import java.text.SimpleDateFormat; import java.util.TimeZone; import org.dpppt.android.calibration.R; +import org.dpppt.android.calibration.handshakes.BackendCalibrationReportRepository; import org.dpppt.android.sdk.BuildConfig; +import org.dpppt.android.sdk.DP3TCalibrationHelper; +import org.dpppt.android.sdk.backend.ResponseCallback; import org.dpppt.android.sdk.internal.AppConfigManager; +import org.dpppt.android.sdk.internal.backend.models.GaenRequest; +import org.dpppt.android.sdk.internal.nearby.GoogleExposureClient; +import org.dpppt.android.sdk.util.DateUtil; public class ParametersFragment extends Fragment { - AppConfigManager appConfigManager; + private static final int RESOLUTION_REQUEST_CODE = 123; - AppCompatSeekBar attenuationBucket1Seeekbar; - EditText attenuationBucket1Text; - AppCompatSeekBar attenuationBucket2Seeekbar; - EditText attenuationBucket2Text; + AppConfigManager appConfigManager; + EditText experimentIdEditText; + EditText deviceIdEditText; public static ParametersFragment newInstance() { return new ParametersFragment(); @@ -57,75 +63,14 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat appConfigManager = AppConfigManager.getInstance(getContext()); - attenuationBucket1Seeekbar = view.findViewById(R.id.parameter_seekbar_attenuation_bucket1); - attenuationBucket1Text = view.findViewById(R.id.parameter_seekbar_attenuation_bucket1_value); - - attenuationBucket1Seeekbar.setMax(254); - attenuationBucket1Seeekbar.setProgress(appConfigManager.getAttenuationThresholdLow()); - attenuationBucket1Seeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - setBucket1Value(progress); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { } - }); - attenuationBucket1Text.setText(Integer.toString(appConfigManager.getAttenuationThresholdLow())); - attenuationBucket1Text.setOnEditorActionListener((v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_DONE) { - String input = attenuationBucket1Text.getText().toString(); - if (input.length() == 0) return true; - try { - int value = Integer.parseInt(input); - attenuationBucket1Seeekbar.setProgress(value); - hideKeyboard(v); - } catch (NumberFormatException e) { - e.printStackTrace(); - } - return true; - } - return false; - }); - - attenuationBucket2Seeekbar = view.findViewById(R.id.parameter_seekbar_attenuation_bucket2); - attenuationBucket2Text = view.findViewById(R.id.parameter_seekbar_attenuation_bucket2_value); - - attenuationBucket2Seeekbar.setMax(255); - attenuationBucket2Seeekbar.setProgress(appConfigManager.getAttenuationThresholdMedium()); - attenuationBucket2Seeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - int min = 1; - if (progress < min) progress = min; - setBucket2Value(progress); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { } - }); - attenuationBucket2Text.setText(Integer.toString(appConfigManager.getAttenuationThresholdMedium())); - attenuationBucket2Text.setOnEditorActionListener((v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_DONE) { - String input = attenuationBucket2Text.getText().toString(); - if (input.length() == 0) return true; - try { - int value = Integer.parseInt(input); - attenuationBucket2Seeekbar.setProgress(value); - hideKeyboard(v); - } catch (NumberFormatException e) { - e.printStackTrace(); - } - return true; - } - return false; - }); + experimentIdEditText = view.findViewById(R.id.experiment_id); + deviceIdEditText = view.findViewById(R.id.experiment_device_id); + + Button deanonymizationButton = view.findViewById(R.id.deanonymization_key_upload_button); + deanonymizationButton.setOnClickListener(v -> uploadKeys()); + + experimentIdEditText.setText(DP3TCalibrationHelper.getInstance(getContext()).getExperimentName()); + deviceIdEditText.setText(DP3TCalibrationHelper.getInstance(getContext()).getCalibrationTestDeviceName()); TextView version_info = view.findViewById(R.id.version_info); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @@ -135,21 +80,49 @@ public void onStopTrackingTouch(SeekBar seekBar) { } BuildConfig.BUILD_TYPE); } - private void setBucket1Value(int thresholdLow) { - int thresholdMedium = Math.max(attenuationBucket2Seeekbar.getProgress(), thresholdLow + 1); - appConfigManager.setAttenuationThresholds(thresholdLow, thresholdMedium); - attenuationBucket1Text.setText(Integer.toString(thresholdLow)); - attenuationBucket2Seeekbar.setProgress(thresholdMedium); + private void uploadKeys() { + String experimentId = experimentIdEditText.getText().toString(); + String deviceId = deviceIdEditText.getText().toString(); + DP3TCalibrationHelper.getInstance(getContext()).setExperimentName(experimentId); + DP3TCalibrationHelper.getInstance(getContext()).setCalibrationTestDeviceName(deviceId); + experimentIdEditText.setText(DP3TCalibrationHelper.getInstance(getContext()).getExperimentName()); + deviceIdEditText.setText(DP3TCalibrationHelper.getInstance(getContext()).getCalibrationTestDeviceName()); + String name = "experiment_" + DP3TCalibrationHelper.getInstance(getContext()).getExperimentName() + "_" + + DP3TCalibrationHelper.getInstance(getContext()).getCalibrationTestDeviceName(); + GoogleExposureClient.getInstance(getContext()) + .getTemporaryExposureKeyHistory(getActivity(), RESOLUTION_REQUEST_CODE, temporaryExposureKeys -> { + ProgressDialog progressDialog = new ProgressDialog(getContext()); + progressDialog.show(); + GaenRequest exposeeListRequest = + new GaenRequest(temporaryExposureKeys, DateUtil.getCurrentRollingStartNumber()); + new BackendCalibrationReportRepository(requireContext()) + .addGaenExposee(exposeeListRequest, name, + new ResponseCallback() { + @Override + public void onSuccess(Void response) { + progressDialog.dismiss(); + Toast.makeText(getContext(), "Uploaded keys!", Toast.LENGTH_LONG).show(); + } + + @Override + public void onError(Throwable throwable) { + progressDialog.dismiss(); + Toast.makeText(getContext(), throwable.getLocalizedMessage(), Toast.LENGTH_LONG) + .show(); + } + }); + }, e -> { + Toast.makeText(getContext(), e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); + }); } - private void setBucket2Value(int thresholdMedium) { - int thresholdLow = Math.min(attenuationBucket1Seeekbar.getProgress(), thresholdMedium - 1); - appConfigManager.setAttenuationThresholds(thresholdLow, thresholdMedium); - attenuationBucket2Text.setText(Integer.toString(thresholdMedium)); - attenuationBucket1Seeekbar.setProgress(thresholdLow); + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode == RESOLUTION_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + uploadKeys(); + } } - private void hideKeyboard(View view) { InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Activity.INPUT_METHOD_SERVICE); inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); diff --git a/calibration-app/app/src/main/res/color/selector_tracking_button_color.xml b/calibration-app/app/src/main/res/color/selector_tracing_button_color.xml similarity index 100% rename from calibration-app/app/src/main/res/color/selector_tracking_button_color.xml rename to calibration-app/app/src/main/res/color/selector_tracing_button_color.xml diff --git a/calibration-app/app/src/main/res/color/selector_tracking_text_color.xml b/calibration-app/app/src/main/res/color/selector_tracing_text_color.xml similarity index 100% rename from calibration-app/app/src/main/res/color/selector_tracking_text_color.xml rename to calibration-app/app/src/main/res/color/selector_tracing_text_color.xml diff --git a/calibration-app/app/src/main/res/layout/fragment_home.xml b/calibration-app/app/src/main/res/layout/fragment_home.xml index ed6181f4..30d03746 100644 --- a/calibration-app/app/src/main/res/layout/fragment_home.xml +++ b/calibration-app/app/src/main/res/layout/fragment_home.xml @@ -59,11 +59,11 @@ tools:text="This textview displays the status of the STAR SDK on one or multiple lines." />