From 3ab7d05632a914f5383ec18a1097803f9c87a6c9 Mon Sep 17 00:00:00 2001 From: Fabian Aggeler Date: Tue, 14 Apr 2020 14:09:22 +0200 Subject: [PATCH 001/261] Initial alpha version of DP3T-SDK for Android --- .github/workflows/android.yml | 42 ++ .github/workflows/appcenter.yml | 26 ++ .gitignore | 1 - README.md | 157 ++++++- calibration-app/.gitignore | 9 + calibration-app/app/.gitignore | 1 + calibration-app/app/build.gradle | 68 +++ calibration-app/app/proguard-rules.pro | 21 + .../app/src/main/AndroidManifest.xml | 27 ++ .../android/calibration/MainActivity.java | 66 +++ .../android/calibration/MainApplication.java | 59 +++ .../controls/ControlsFragment.java | 400 ++++++++++++++++++ .../controls/ExposedDialogFragment.java | 136 ++++++ .../handshakes/HandshakesFragment.java | 159 +++++++ .../android/calibration/logs/LogsAdapter.java | 137 ++++++ .../calibration/logs/LogsFragment.java | 127 ++++++ .../parameters/ParametersFragment.java | 214 ++++++++++ .../util/DatePickerFragmentDialog.java | 65 +++ .../android/calibration/util/DialogUtil.java | 40 ++ .../calibration/util/NotificationUtil.java | 56 +++ .../util/OnTextChangedListener.java | 21 + .../calibration/util/PreferencesUtil.java | 26 ++ .../calibration/util/RequirementsUtil.java | 36 ++ .../selector_bottom_navigation_item_color.xml | 11 + .../main/res/color/selector_button_red.xml | 11 + .../color/selector_default_button_color.xml | 11 + .../selector_requirements_button_color.xml | 11 + ...elector_requirements_button_icon_color.xml | 11 + .../selector_requirements_text_color.xml | 11 + .../color/selector_tracking_button_color.xml | 11 + .../color/selector_tracking_text_color.xml | 11 + .../drawable-v24/ic_launcher_foreground.xml | 36 ++ .../src/main/res/drawable/ic_check_circle.xml | 15 + .../app/src/main/res/drawable/ic_controls.xml | 15 + .../app/src/main/res/drawable/ic_error.xml | 15 + .../src/main/res/drawable/ic_handshakes.xml | 15 + .../res/drawable/ic_launcher_background.xml | 176 ++++++++ .../app/src/main/res/drawable/ic_logs.xml | 15 + .../src/main/res/drawable/ic_parameters.xml | 15 + .../app/src/main/res/drawable/ic_refresh.xml | 15 + .../src/main/res/drawable/ic_to_bottom.xml | 15 + .../selector_requirements_button_icon.xml | 11 + .../app/src/main/res/layout/activity_main.xml | 26 ++ .../res/layout/dialog_fragment_exposed.xml | 93 ++++ .../main/res/layout/fragment_handshakes.xml | 59 +++ .../app/src/main/res/layout/fragment_home.xml | 169 ++++++++ .../app/src/main/res/layout/fragment_logs.xml | 62 +++ .../main/res/layout/fragment_parameters.xml | 102 +++++ .../src/main/res/layout/view_log_entry.xml | 62 +++ .../main/res/menu/menu_navigation_main.xml | 29 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 11 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 11 + .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7909 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11873 bytes .../app/src/main/res/values/colors.xml | 23 + .../app/src/main/res/values/dimens.xml | 20 + .../app/src/main/res/values/strings.xml | 63 +++ .../app/src/main/res/values/styles.xml | 65 +++ calibration-app/build.gradle | 35 ++ calibration-app/gradle.properties | 26 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 12 + calibration-app/gradlew | 172 ++++++++ calibration-app/gradlew.bat | 84 ++++ calibration-app/screenshots/1.png | Bin 0 -> 126496 bytes calibration-app/screenshots/2.png | Bin 0 -> 86061 bytes calibration-app/screenshots/3.png | Bin 0 -> 290543 bytes calibration-app/settings.gradle | 9 + calibration-app/testKeystore | Bin 0 -> 2099 bytes dp3t-sdk/.gitignore | 75 ++++ dp3t-sdk/build.gradle | 26 ++ dp3t-sdk/gradle.properties | 20 + dp3t-sdk/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + dp3t-sdk/gradlew | 172 ++++++++ dp3t-sdk/gradlew.bat | 84 ++++ dp3t-sdk/sdk/.gitignore | 2 + dp3t-sdk/sdk/build.gradle | 61 +++ dp3t-sdk/sdk/consumer-rules.pro | 2 + dp3t-sdk/sdk/proguard-rules.pro | 21 + .../sdk/src/androidTest/AndroidManifest.xml | 28 ++ .../android/sdk/internal/AESBenchmark.java | 85 ++++ .../android/sdk/internal/CryptoBenchmark.java | 54 +++ .../sdk/internal/crypto/CryptoTest.java | 135 ++++++ .../sdk/internal/logger/LogDatabase.java | 166 ++++++++ .../android/sdk/internal/logger/LogEntry.java | 39 ++ .../android/sdk/internal/logger/Logger.java | 86 ++++ dp3t-sdk/sdk/src/main/AndroidManifest.xml | 37 ++ .../main/java/org/dpppt/android/sdk/DP3T.java | 240 +++++++++++ .../org/dpppt/android/sdk/TracingStatus.java | 77 ++++ .../sdk/internal/AppConfigManager.java | 209 +++++++++ .../sdk/internal/BluetoothAdvertiseMode.java | 25 ++ .../sdk/internal/BluetoothTxPowerLevel.java | 26 ++ .../android/sdk/internal/BroadcastHelper.java | 22 + .../android/sdk/internal/SyncWorker.java | 100 +++++ .../android/sdk/internal/TracingService.java | 246 +++++++++++ .../TracingServiceBroadcastReceiver.java | 38 ++ .../internal/backend/BackendRepository.java | 68 +++ .../sdk/internal/backend/BackendService.java | 26 ++ .../internal/backend/CallbackListener.java | 14 + .../internal/backend/DiscoveryRepository.java | 62 +++ .../internal/backend/DiscoveryService.java | 21 + .../sdk/internal/backend/Repository.java | 49 +++ .../internal/backend/ResponseException.java | 27 ++ .../sdk/internal/backend/models/Action.java | 12 + .../backend/models/ApplicationInfo.java | 26 ++ .../backend/models/ApplicationsList.java | 23 + .../internal/backend/models/ExposedList.java | 19 + .../sdk/internal/backend/models/Exposee.java | 29 ++ .../backend/models/ExposeeAuthData.java | 21 + .../backend/models/ExposeeRequest.java | 35 ++ .../sdk/internal/crypto/CryptoModule.java | 263 ++++++++++++ .../sdk/internal/crypto/EphIdsForDay.java | 17 + .../android/sdk/internal/crypto/SKList.java | 16 + .../sdk/internal/database/Contacts.java | 35 ++ .../sdk/internal/database/Database.java | 202 +++++++++ .../internal/database/DatabaseOpenHelper.java | 75 ++++ .../sdk/internal/database/DatabaseThread.java | 47 ++ .../sdk/internal/database/Handshakes.java | 36 ++ .../internal/database/InsertTransaction.java | 38 ++ .../sdk/internal/database/KnownCases.java | 34 ++ .../sdk/internal/database/ResultListener.java | 13 + .../sdk/internal/database/Transaction.java | 37 ++ .../sdk/internal/database/models/Contact.java | 40 ++ .../internal/database/models/Handshake.java | 40 ++ .../internal/database/models/KnownCase.java | 38 ++ .../android/sdk/internal/gatt/BleClient.java | 152 +++++++ .../android/sdk/internal/gatt/BleServer.java | 181 ++++++++ .../sdk/internal/gatt/GattConnectionTask.java | 157 +++++++ .../internal/gatt/GattConnectionThread.java | 56 +++ .../android/sdk/internal/logger/LogLevel.java | 46 ++ .../android/sdk/internal/util/Base64Util.java | 21 + .../android/sdk/internal/util/DayDate.java | 99 +++++ .../sdk/internal/util/DayDateJsonAdapter.java | 33 ++ .../sdk/internal/util/ProcessUtil.java | 33 ++ .../src/main/res/drawable/ic_handshakes.xml | 16 + dp3t-sdk/sdk/src/main/res/values/strings.xml | 19 + .../android/sdk/internal/logger/Logger.java | 43 ++ dp3t-sdk/settings.gradle | 2 + 139 files changed, 7759 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/android.yml create mode 100644 .github/workflows/appcenter.yml create mode 100644 calibration-app/.gitignore create mode 100644 calibration-app/app/.gitignore create mode 100644 calibration-app/app/build.gradle create mode 100644 calibration-app/app/proguard-rules.pro create mode 100644 calibration-app/app/src/main/AndroidManifest.xml create mode 100644 calibration-app/app/src/main/java/org/dpppt/android/calibration/MainActivity.java create mode 100644 calibration-app/app/src/main/java/org/dpppt/android/calibration/MainApplication.java create mode 100644 calibration-app/app/src/main/java/org/dpppt/android/calibration/controls/ControlsFragment.java create mode 100644 calibration-app/app/src/main/java/org/dpppt/android/calibration/controls/ExposedDialogFragment.java create mode 100644 calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/HandshakesFragment.java create mode 100644 calibration-app/app/src/main/java/org/dpppt/android/calibration/logs/LogsAdapter.java create mode 100644 calibration-app/app/src/main/java/org/dpppt/android/calibration/logs/LogsFragment.java create mode 100644 calibration-app/app/src/main/java/org/dpppt/android/calibration/parameters/ParametersFragment.java create mode 100644 calibration-app/app/src/main/java/org/dpppt/android/calibration/util/DatePickerFragmentDialog.java create mode 100644 calibration-app/app/src/main/java/org/dpppt/android/calibration/util/DialogUtil.java create mode 100644 calibration-app/app/src/main/java/org/dpppt/android/calibration/util/NotificationUtil.java create mode 100644 calibration-app/app/src/main/java/org/dpppt/android/calibration/util/OnTextChangedListener.java create mode 100644 calibration-app/app/src/main/java/org/dpppt/android/calibration/util/PreferencesUtil.java create mode 100644 calibration-app/app/src/main/java/org/dpppt/android/calibration/util/RequirementsUtil.java create mode 100644 calibration-app/app/src/main/res/color/selector_bottom_navigation_item_color.xml create mode 100644 calibration-app/app/src/main/res/color/selector_button_red.xml create mode 100644 calibration-app/app/src/main/res/color/selector_default_button_color.xml create mode 100644 calibration-app/app/src/main/res/color/selector_requirements_button_color.xml create mode 100644 calibration-app/app/src/main/res/color/selector_requirements_button_icon_color.xml create mode 100644 calibration-app/app/src/main/res/color/selector_requirements_text_color.xml create mode 100644 calibration-app/app/src/main/res/color/selector_tracking_button_color.xml create mode 100644 calibration-app/app/src/main/res/color/selector_tracking_text_color.xml create mode 100644 calibration-app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 calibration-app/app/src/main/res/drawable/ic_check_circle.xml create mode 100644 calibration-app/app/src/main/res/drawable/ic_controls.xml create mode 100644 calibration-app/app/src/main/res/drawable/ic_error.xml create mode 100644 calibration-app/app/src/main/res/drawable/ic_handshakes.xml create mode 100644 calibration-app/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 calibration-app/app/src/main/res/drawable/ic_logs.xml create mode 100644 calibration-app/app/src/main/res/drawable/ic_parameters.xml create mode 100644 calibration-app/app/src/main/res/drawable/ic_refresh.xml create mode 100644 calibration-app/app/src/main/res/drawable/ic_to_bottom.xml create mode 100644 calibration-app/app/src/main/res/drawable/selector_requirements_button_icon.xml create mode 100644 calibration-app/app/src/main/res/layout/activity_main.xml create mode 100644 calibration-app/app/src/main/res/layout/dialog_fragment_exposed.xml create mode 100644 calibration-app/app/src/main/res/layout/fragment_handshakes.xml create mode 100644 calibration-app/app/src/main/res/layout/fragment_home.xml create mode 100644 calibration-app/app/src/main/res/layout/fragment_logs.xml create mode 100644 calibration-app/app/src/main/res/layout/fragment_parameters.xml create mode 100644 calibration-app/app/src/main/res/layout/view_log_entry.xml create mode 100644 calibration-app/app/src/main/res/menu/menu_navigation_main.xml create mode 100644 calibration-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 calibration-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 calibration-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 calibration-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 calibration-app/app/src/main/res/values/colors.xml create mode 100644 calibration-app/app/src/main/res/values/dimens.xml create mode 100644 calibration-app/app/src/main/res/values/strings.xml create mode 100644 calibration-app/app/src/main/res/values/styles.xml create mode 100644 calibration-app/build.gradle create mode 100644 calibration-app/gradle.properties create mode 100644 calibration-app/gradle/wrapper/gradle-wrapper.jar create mode 100644 calibration-app/gradle/wrapper/gradle-wrapper.properties create mode 100755 calibration-app/gradlew create mode 100644 calibration-app/gradlew.bat create mode 100644 calibration-app/screenshots/1.png create mode 100644 calibration-app/screenshots/2.png create mode 100644 calibration-app/screenshots/3.png create mode 100644 calibration-app/settings.gradle create mode 100644 calibration-app/testKeystore create mode 100644 dp3t-sdk/.gitignore create mode 100644 dp3t-sdk/build.gradle create mode 100644 dp3t-sdk/gradle.properties create mode 100644 dp3t-sdk/gradle/wrapper/gradle-wrapper.jar create mode 100644 dp3t-sdk/gradle/wrapper/gradle-wrapper.properties create mode 100755 dp3t-sdk/gradlew create mode 100644 dp3t-sdk/gradlew.bat create mode 100644 dp3t-sdk/sdk/.gitignore create mode 100644 dp3t-sdk/sdk/build.gradle create mode 100644 dp3t-sdk/sdk/consumer-rules.pro create mode 100644 dp3t-sdk/sdk/proguard-rules.pro create mode 100644 dp3t-sdk/sdk/src/androidTest/AndroidManifest.xml create mode 100644 dp3t-sdk/sdk/src/androidTest/java/org/dpppt/android/sdk/internal/AESBenchmark.java create mode 100644 dp3t-sdk/sdk/src/androidTest/java/org/dpppt/android/sdk/internal/CryptoBenchmark.java create mode 100644 dp3t-sdk/sdk/src/androidTest/java/org/dpppt/android/sdk/internal/crypto/CryptoTest.java create mode 100644 dp3t-sdk/sdk/src/calibration/java/org/dpppt/android/sdk/internal/logger/LogDatabase.java create mode 100644 dp3t-sdk/sdk/src/calibration/java/org/dpppt/android/sdk/internal/logger/LogEntry.java create mode 100644 dp3t-sdk/sdk/src/calibration/java/org/dpppt/android/sdk/internal/logger/Logger.java create mode 100644 dp3t-sdk/sdk/src/main/AndroidManifest.xml create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/DP3T.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/TracingStatus.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/AppConfigManager.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/BluetoothAdvertiseMode.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/BluetoothTxPowerLevel.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/BroadcastHelper.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/SyncWorker.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/TracingService.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/TracingServiceBroadcastReceiver.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/backend/BackendRepository.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/backend/BackendService.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/backend/CallbackListener.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/backend/DiscoveryRepository.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/backend/DiscoveryService.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/backend/Repository.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/backend/ResponseException.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/backend/models/Action.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/backend/models/ApplicationInfo.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/backend/models/ApplicationsList.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/backend/models/ExposedList.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/backend/models/Exposee.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/backend/models/ExposeeAuthData.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/backend/models/ExposeeRequest.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/crypto/CryptoModule.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/crypto/EphIdsForDay.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/crypto/SKList.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/database/Contacts.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/database/Database.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/database/DatabaseOpenHelper.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/database/DatabaseThread.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/database/Handshakes.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/database/InsertTransaction.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/database/KnownCases.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/database/ResultListener.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/database/Transaction.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/database/models/Contact.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/database/models/Handshake.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/database/models/KnownCase.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/gatt/BleClient.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/gatt/BleServer.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/gatt/GattConnectionTask.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/gatt/GattConnectionThread.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/logger/LogLevel.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/util/Base64Util.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/util/DayDate.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/util/DayDateJsonAdapter.java create mode 100644 dp3t-sdk/sdk/src/main/java/org/dpppt/android/sdk/internal/util/ProcessUtil.java create mode 100644 dp3t-sdk/sdk/src/main/res/drawable/ic_handshakes.xml create mode 100644 dp3t-sdk/sdk/src/main/res/values/strings.xml create mode 100644 dp3t-sdk/sdk/src/production/java/org/dpppt/android/sdk/internal/logger/Logger.java create mode 100644 dp3t-sdk/settings.gradle diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 00000000..ff472c00 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,42 @@ +name: Android Tests + +on: + push: + branches: [ master, develop ] + pull_request: + branches: [ master, develop ] + +jobs: + build: + name: "Build APK" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Build with Gradle + run: cd calibration-app; ./gradlew assembleDebug + - name: Upload APK + uses: actions/upload-artifact@v1.0.0 + with: + name: app + path: calibration-app/app/build/outputs/apk/debug/app-debug.apk + + test: + name: "Run Tests" + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + - name: set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Run tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 29 + script: cd dp3t-sdk; ./gradlew connectedAndroidTest diff --git a/.github/workflows/appcenter.yml b/.github/workflows/appcenter.yml new file mode 100644 index 00000000..229221e4 --- /dev/null +++ b/.github/workflows/appcenter.yml @@ -0,0 +1,26 @@ +name: Android Build + +on: + push: + branches: [ master, develop ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Build with Gradle + run: cd calibration-app; ./gradlew assembleRelease + - name: upload artefact to App Center + uses: wzieba/AppCenter-Github-Action@v1.0.0 + with: + appName: ${{secrets.APPCENTER_ORGANIZATION}}/${{secrets.APPCENTER_APP}} + token: ${{secrets.APPCENTER_API_TOKEN}} + group: Internal + file: calibration-app/app/build/outputs/apk/release/app-release.apk diff --git a/.gitignore b/.gitignore index a1c2a238..9ca78015 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ .mtj.tmp/ # Package Files # -*.jar *.war *.nar *.ear diff --git a/README.md b/README.md index 5f0aa657..d793a349 100644 --- a/README.md +++ b/README.md @@ -1 +1,156 @@ -# dp3t-sdk-android \ No newline at end of file +# DP3T-SDK for Android +[![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://github.com/DP-3T/dp3t-sdk-android/blob/master/LICENSE) +![Android Build](https://github.com/DP-3T/dp3t-sdk-android/workflows/Android%20Build/badge.svg) +![Android Tests](https://github.com/DP-3T/dp3t-sdk-android/workflows/Android%20Tests/badge.svg) + +## DP3T +The Decentralised Privacy-Preserving Proximity Tracing (DP-3T) project is an open protocol for COVID-19 proximity tracing using Bluetooth Low Energy functionality on mobile devices that ensures personal data and computation stays entirely on an individual's phone. It was produced by a core team of over 25 scientists and academic researchers from across Europe. It has also been scrutinized and improved by the wider community. + +DP-3T is a free-standing effort started at EPFL and ETHZ that produced this protocol and that is implementing it in an open-sourced app and server. + + +## Introduction +This is the first implementation of the DP-3T "low bandwidth" protocol. The current implementation does not use the as yet unreleased "Contact Tracing" API of Apple/Google--**and has limitations as a result**. Our "hybrid approach" uses Bluetooth Low Energy (BLE) to exchange `EphID`s. It uses advertisements whenever possible and falls back to GATT Server connections if not possible to transmit/collect an `EphID` this way (e.g., on iOS devices when the app is in background). This approach can result in higher energy consumption and scalability issues and will be replaced by the Apple/Google API. + +Our immediate roadmap is: to support the Apple/Google wire protocol, to be forward-compatible, and to support the actual Apple/Google API as soon as it is released to iOS and Android devices. + +## Repositories +* Android SDK & Calibration app: [dp3t-sdk-android](https://github.com/DP-3T/dp3t-sdk-android) +* iOS SDK & Calibration app: [dp3t-sdk-ios](https://github.com/DP-3T/dp3t-sdk-ios) +* Android Demo App: [dp3t-app-android](https://github.com/DP-3T/dp3t-app-android) +* iOS Demo App: [dp3t-app-ios](https://github.com/DP-3T/dp3t-app-ios) +* Backend SDK: [dp3t-sdk-backend](https://github.com/DP-3T/dp3t-sdk-backend) + +## Work in Progress +The DP3T-SDK for Android contains alpha-quality code only and is not yet complete. It has not yet been reviewed or audited for security and compatibility. We are both continuing the development and have started a security review. This project is truly open-source and we welcome any feedback on the code regarding both the implementation and security aspects. +This repository contains the open prototype SDK, so please focus your feedback for this repository on implementation issues. + +## Further Documentation +The full set of documents for DP3T is at https://github.com/DP-3T/documents. Please refer to the technical documents and whitepapers for a description of the implementation. + +## Architecture +A central discovery service is hosted on [Github](https://github.com/DP-3T/dp3t-discovery). This server provides the necessary information for the SDK to initialize itself. After the SDK loads the base url for its own backend, it will load the infected list from there, as well as post if a user is infected. This will also allow apps to fetch lists from other backend systems participating in this scheme and can handle roaming of users. + +## 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. + +

+ + + +

+ +## Function overview + +### Initialization +Name | Description | Function Name +---- | ----------- | ------------- +initWithAppId | Initializes the SDK and configures it | `public static void init(Context context, String appId)` + +### Methods +Name | Description | Function Name +---- | ----------- | ------------- +start | Starts Bluetooth tracing | `public static void start(Context context)` +stop | Stops Bluetooth tracing | `public static void stop(Context context)` +sync | Pro-actively triggers sync with backend to refresh exposed list | `public static void sync(Context context)` +status | Returns a TracingStatus-Object describing the current state. This contains:
- `numberOfHandshakes` : `int`
- `advertising` : `boolean`
- `receiving` : `boolean`
- `wasContactExposed`:`boolean`
- `lastSyncUpdate`:`long`
- `errors` (permission, bluetooth disabled, no network, ...) : `List` | `public static TracingStatus getStatus(Context context)` +I was exposed | This method must be called upon positive test. | `public static void sendIWasExposed(Context context, Date onset, ExposeeAuthData exposeeAuthData, CallbackListener callback)` +reset | Removes all SDK related data (key and database) and de-initializes SDK | `public static void reset(Context context)` + +### Broadcast +Name | Description | Function Name +---- | ----------- | ------------- +status update | Status was updated; new status can be fetched with the `status` method | Register for Broadcast with the `IntentFilter` returned by `public static IntentFilter getUpdateIntentFilter()` + + +## Building a AAR +To build an aar file that you can include in your project use in the folder dp3t-sdk: +```sh +$ ./gradlew assemble +``` +The library is generated under sdk/build/outputs/aar + +## Integrating into a Project +Include the built aar file by adding it to your project. Make sure that you also include the following dependencies: +```groovy +implementation 'androidx.core:core:1.2.0' +implementation "androidx.security:security-crypto:1.0.0-beta01" +implementation 'androidx.work:work-runtime:2.3.4' + +implementation 'com.squareup.retrofit2:retrofit:2.6.2' +implementation 'com.squareup.retrofit2:converter-gson:2.6.2' +``` + +## Using the SDK + +### Initialization +In your Application.onCreate() you have to initialize the SDK with: +```java +D3PTTracing.init(getContext(), "com.example.your.app"); +``` +The provided app name has to be registered in the discovery service on [Github](https://github.com/DP-3T/dp3t-discovery/blob/master/discovery.json) + +### Start / Stop tracing +To start and stop tracing use +```java +D3PTTracing.start(getContext()); +D3PTTracing.stop(getContext()); +``` +Make sure that the user has the permission Manifest.permission.ACCESS_FINE_LOCATION granted (this coarse-grained permission is required for any app with Bluetooth activity; our SDK uses BLE beaconing but does not require any "location" data), Bluetooth is enabled and BatteryOptimization is disabled. BatteryOptimization can be checked with +```java +PowerManager powerManager = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE); +boolean batteryOptDeact = powerManager.isIgnoringBatteryOptimizations(getContext().getPackageName()); +``` +and for asking the user to disable the optimization use: +```java +startActivity(new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:" + getContext().getPackageName()))); +``` + +Tracing is automatically restarted if the phone is rebooted by the SDK, it is enough to call `start()` once from your app. + +### Customize tracing notification +The tracing happens in a foreground service and therefore displays a notification. This notification can be customized by defining the following string resources in your project: +```xml + + + @string/app_name + @string/app_name + @string/foreground_service_notification_text + +``` +To change the notification icon add your custom ic_handshakes drawable to the project. + +### Checking the current tracing status +```java +TracingStatus status = D3PTTracing.getStatus(getContext()); +``` +The TracingStatus object contains all information of the current tracing status. + +To get notified when the status changes, you can register a broadcast receiver with +```java +getContext().registerReceiver(broadcastReceiver, D3PTTracing.getUpdateIntentFilter()); +``` + +### Report user exposed +```java +D3PTTracing.sendIWasExposed(getContext(), null, new CallbackListener() { + @Override + public void onSuccess(Void response) { + } + + @Override + public void onError(Throwable throwable) { + } + }); +``` + +### Sync with backend for exposed user +The SDK automatically registers a periodic Job to sync with the backend for new exposed users. If you want to trigger a sync manually (e.g., upon a push from your backend) you can use: +```java +D3PTTracing.sync(getContext()); +``` +Make sure you do not call this method on the UI thread, because it will perform the sync synchronously. + +## License +This project is licensed under the terms of the MPL 2 license. See the [LICENSE](LICENSE) file. diff --git a/calibration-app/.gitignore b/calibration-app/.gitignore new file mode 100644 index 00000000..0f1d1370 --- /dev/null +++ b/calibration-app/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/calibration-app/app/.gitignore b/calibration-app/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/calibration-app/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/calibration-app/app/build.gradle b/calibration-app/app/build.gradle new file mode 100644 index 00000000..622a4ee9 --- /dev/null +++ b/calibration-app/app/build.gradle @@ -0,0 +1,68 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ + +apply plugin: 'com.android.application' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.3" + + defaultConfig { + applicationId "org.dpppt.android.calibration" + minSdkVersion 23 + targetSdkVersion 29 + versionCode 1 + versionName "0.1" + + missingDimensionStrategy 'version', 'calibration' + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + signingConfigs { + release { + storeFile file('../testKeystore') + storePassword '123456' + keyAlias 'keyAlias' + keyPassword '123456' + } + } + buildTypes { + release { + signingConfig signingConfigs.release + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation project(':dp3t-sdk') + + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core:1.2.0' + implementation 'androidx.fragment:fragment:1.2.4' + implementation "androidx.security:security-crypto:1.0.0-beta01" + implementation 'androidx.work:work-runtime:2.3.4' + + implementation 'com.squareup.retrofit2:retrofit:2.6.2' + implementation 'com.squareup.retrofit2:converter-gson:2.6.2' + + implementation 'com.google.android.material:material:1.1.0' +} diff --git a/calibration-app/app/proguard-rules.pro b/calibration-app/app/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/calibration-app/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/calibration-app/app/src/main/AndroidManifest.xml b/calibration-app/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..aaba718a --- /dev/null +++ b/calibration-app/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + \ 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 new file mode 100644 index 00000000..3ce6aaee --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/MainActivity.java @@ -0,0 +1,66 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ + +package org.dpppt.android.calibration; + +import androidx.appcompat.app.AppCompatActivity; +import android.os.Bundle; + +import com.google.android.material.bottomnavigation.BottomNavigationView; + +import org.dpppt.android.calibration.controls.ControlsFragment; +import org.dpppt.android.calibration.handshakes.HandshakesFragment; +import org.dpppt.android.calibration.logs.LogsFragment; +import org.dpppt.android.calibration.parameters.ParametersFragment; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + setupNavigationView(); + + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction() + .add(R.id.main_fragment_container, ControlsFragment.newInstance()) + .commit(); + } + } + + private void setupNavigationView() { + BottomNavigationView navigationView = findViewById(R.id.main_navigation_view); + navigationView.inflateMenu(R.menu.menu_navigation_main); + + navigationView.setOnNavigationItemSelectedListener(item -> { + switch (item.getItemId()) { + case R.id.action_controls: + getSupportFragmentManager().beginTransaction() + .replace(R.id.main_fragment_container, ControlsFragment.newInstance()) + .commit(); + break; + case R.id.action_parameters: + getSupportFragmentManager().beginTransaction() + .replace(R.id.main_fragment_container, ParametersFragment.newInstance()) + .commit(); + break; + case R.id.action_handshakes: + getSupportFragmentManager().beginTransaction() + .replace(R.id.main_fragment_container, HandshakesFragment.newInstance()) + .commit(); + break; + case R.id.action_logs: + getSupportFragmentManager().beginTransaction() + .replace(R.id.main_fragment_container, LogsFragment.newInstance()) + .commit(); + break; + } + return true; + }); + } + +} 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 new file mode 100644 index 00000000..3b3beb23 --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/MainApplication.java @@ -0,0 +1,59 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ +package org.dpppt.android.calibration; + +import android.app.Application; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +import org.dpppt.android.calibration.util.NotificationUtil; +import org.dpppt.android.calibration.util.PreferencesUtil; +import org.dpppt.android.sdk.DP3T; +import org.dpppt.android.sdk.internal.logger.LogLevel; +import org.dpppt.android.sdk.internal.logger.Logger; +import org.dpppt.android.sdk.internal.util.ProcessUtil; + +public class MainApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + if (ProcessUtil.isMainProcess(this)) { + registerReceiver(sdkReceiver, DP3T.getUpdateIntentFilter()); + initDP3T(this); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationUtil.createNotificationChannel(this); + } + Logger.init(getApplicationContext(), LogLevel.DEBUG); + } + + public static void initDP3T(Context context) { + DP3T.init(context, "org.dpppt.demo", true); + } + + @Override + public void onTerminate() { + if (ProcessUtil.isMainProcess(this)) { + unregisterReceiver(sdkReceiver); + } + super.onTerminate(); + } + + private BroadcastReceiver sdkReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DP3T.getStatus(context).wasContactExposed() && !PreferencesUtil.isExposedNotificationShown(context)) { + NotificationUtil.showNotification(context, R.string.push_exposed_title, + R.string.push_exposed_text, R.drawable.ic_handshakes); + PreferencesUtil.setExposedNotificationShown(context); + } + } + }; + +} \ No newline at end of file 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 new file mode 100644 index 00000000..fe80bd40 --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/controls/ControlsFragment.java @@ -0,0 +1,400 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ +package org.dpppt.android.calibration.controls; + +import android.Manifest; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.Settings; +import android.text.Editable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextWatcher; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Switch; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; + +import java.io.FileNotFoundException; +import java.io.OutputStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; + +import org.dpppt.android.calibration.MainApplication; +import org.dpppt.android.calibration.R; +import org.dpppt.android.calibration.util.DialogUtil; +import org.dpppt.android.calibration.util.RequirementsUtil; +import org.dpppt.android.sdk.DP3T; +import org.dpppt.android.sdk.TracingStatus; +import org.dpppt.android.sdk.internal.backend.CallbackListener; +import org.dpppt.android.sdk.internal.backend.models.ExposeeAuthData; + +public class ControlsFragment extends Fragment { + + private static final String TAG = ControlsFragment.class.getCanonicalName(); + + private static final int REQUEST_CODE_PERMISSION_LOCATION = 1; + private static final int REQUEST_CODE_SAVE_DB = 2; + private static final int REQUEST_CODE_REPORT_EXPOSED = 3; + + private static final DateFormat DATE_FORMAT_SYNC = SimpleDateFormat.getDateTimeInstance(); + + private static final String REGEX_VALIDITY_AUTH_CODE = "\\w+"; + private static final int EXPOSED_MIN_DATE_DIFF = -21; + + private BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { + checkPermissionRequirements(); + updateSdkStatus(); + } + } + }; + + private BroadcastReceiver sdkReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateSdkStatus(); + } + }; + + public static ControlsFragment newInstance() { + return new ControlsFragment(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_home, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + setupUi(view); + } + + @Override + public void onResume() { + super.onResume(); + getContext().registerReceiver(bluetoothReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); + getContext().registerReceiver(sdkReceiver, DP3T.getUpdateIntentFilter()); + checkPermissionRequirements(); + updateSdkStatus(); + } + + @Override + public void onPause() { + super.onPause(); + getContext().unregisterReceiver(bluetoothReceiver); + getContext().unregisterReceiver(sdkReceiver); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode == REQUEST_CODE_SAVE_DB && resultCode == Activity.RESULT_OK && data != null) { + Uri uri = data.getData(); + try { + OutputStream targetOut = getContext().getContentResolver().openOutputStream(uri); + DP3T.exportDb(getContext(), targetOut, () -> + new Handler(getContext().getMainLooper()).post(() -> setExportDbLoadingViewVisible(false))); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return; + } else if (requestCode == REQUEST_CODE_REPORT_EXPOSED) { + if (resultCode == Activity.RESULT_OK) { + long onsetDate = data.getLongExtra(ExposedDialogFragment.RESULT_EXTRA_DATE_MILLIS, -1); + String authCodeBase64 = data.getStringExtra(ExposedDialogFragment.RESULT_EXTRA_AUTH_CODE_INPUT_BASE64); + sendExposedUpdate(getContext(), new Date(onsetDate), authCodeBase64); + } + } + } + + private void setupUi(View view) { + Button locationButton = view.findViewById(R.id.home_button_location); + locationButton.setOnClickListener( + v -> requestPermissions(new String[] { Manifest.permission.ACCESS_FINE_LOCATION }, + REQUEST_CODE_PERMISSION_LOCATION)); + + Button batteryButton = view.findViewById(R.id.home_button_battery_optimization); + batteryButton.setOnClickListener( + v -> startActivity(new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:" + getContext().getPackageName())))); + + Button bluetoothButton = view.findViewById(R.id.home_button_bluetooth); + bluetoothButton.setOnClickListener(v -> BluetoothAdapter.getDefaultAdapter().enable()); + + Button refreshButton = view.findViewById(R.id.home_button_sync); + refreshButton.setOnClickListener(v -> resyncSdk()); + + Button buttonStartAdvertising = view.findViewById(R.id.home_button_start_advertising); + buttonStartAdvertising.setOnClickListener(v -> { + DP3T.start(v.getContext(), true, false); + updateSdkStatus(); + }); + + Button buttonStartReceiving = view.findViewById(R.id.home_button_start_receiving); + buttonStartReceiving.setOnClickListener(v -> { + DP3T.start(v.getContext(), false, true); + updateSdkStatus(); + }); + + Button buttonClearData = view.findViewById(R.id.home_button_clear_data); + buttonClearData.setOnClickListener(v -> { + DialogUtil.showConfirmDialog(v.getContext(), R.string.dialog_clear_data_title, + (dialog, which) -> { + DP3T.clearData(v.getContext(), () -> + new Handler(getContext().getMainLooper()).post(this::updateSdkStatus)); + MainApplication.initDP3T(v.getContext()); + }); + }); + + Button buttonSaveDb = view.findViewById(R.id.home_button_export_db); + buttonSaveDb.setOnClickListener(v -> { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.setType("application/sqlite"); + intent.putExtra(Intent.EXTRA_TITLE, "starsdk_sample_db.sqlite"); + startActivityForResult(intent, REQUEST_CODE_SAVE_DB); + setExportDbLoadingViewVisible(true); + }); + + EditText deanonymizationDeviceId = view.findViewById(R.id.deanonymization_device_id); + Switch deanonymizationSwitch = view.findViewById(R.id.deanonymization_switch); + if (DP3T.getCalibrationTestDeviceName(getContext()) != null) { + deanonymizationSwitch.setChecked(true); + deanonymizationDeviceId.setText(DP3T.getCalibrationTestDeviceName(getContext())); + } + deanonymizationSwitch.setOnCheckedChangeListener((compoundButton, enabled) -> { + if (enabled) { + setDeviceId(deanonymizationDeviceId.getText().toString()); + } else { + DP3T.disableCalibrationTestDeviceName(getContext()); + } + }); + deanonymizationDeviceId.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void afterTextChanged(Editable editable) { + if (deanonymizationSwitch.isChecked()) { + setDeviceId(editable.toString()); + } + } + }); + } + + private void setDeviceId(String deviceId) { + if (deviceId.length() > 4) { + deviceId = deviceId.substring(0, 4); + } else { + while (deviceId.length() < 4) { + deviceId = deviceId + " "; + } + } + DP3T.setCalibrationTestDeviceName(getContext(), deviceId); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == REQUEST_CODE_PERMISSION_LOCATION) { + checkPermissionRequirements(); + updateSdkStatus(); + } + } + + private void checkPermissionRequirements() { + View view = getView(); + Context context = getContext(); + if (view == null || context == null) return; + + boolean locationGranted = RequirementsUtil.isLocationPermissionGranted(context); + Button locationButton = view.findViewById(R.id.home_button_location); + locationButton.setEnabled(!locationGranted); + locationButton.setText(locationGranted ? R.string.req_location_permission_granted + : R.string.req_location_permission_ungranted); + + boolean batteryOptDeactivated = RequirementsUtil.isBatteryOptimizationDeactivated(context); + Button batteryButton = view.findViewById(R.id.home_button_battery_optimization); + batteryButton.setEnabled(!batteryOptDeactivated); + batteryButton.setText(batteryOptDeactivated ? R.string.req_battery_deactivated + : R.string.req_battery_deactivated); + + boolean bluetoothActivated = RequirementsUtil.isBluetoothEnabled(); + Button bluetoothButton = view.findViewById(R.id.home_button_bluetooth); + bluetoothButton.setEnabled(!bluetoothActivated); + bluetoothButton.setText(bluetoothActivated ? R.string.req_bluetooth_active + : R.string.req_bluetooth_inactive); + } + + private void resyncSdk() { + new Thread(() -> { + DP3T.sync(getContext()); + new Handler(getContext().getMainLooper()).post(this::updateSdkStatus); + }).start(); + } + + private void updateSdkStatus() { + View view = getView(); + Context context = getContext(); + if (context == null || view == null) return; + + TracingStatus status = DP3T.getStatus(context); + + TextView statusText = view.findViewById(R.id.home_status_text); + statusText.setText(formatStatusString(status)); + + Button buttonStartStopTracking = view.findViewById(R.id.home_button_start_stop_tracking); + boolean isRunning = status.isAdvertising() || status.isReceiving(); + buttonStartStopTracking.setSelected(isRunning); + buttonStartStopTracking.setText(getString(isRunning ? R.string.button_tracking_stop + : R.string.button_tracking_start)); + buttonStartStopTracking.setOnClickListener(v -> { + if (isRunning) { + DP3T.stop(v.getContext()); + } else { + DP3T.start(v.getContext()); + } + updateSdkStatus(); + }); + + Button buttonStartAdvertising = view.findViewById(R.id.home_button_start_advertising); + buttonStartAdvertising.setEnabled(!isRunning); + Button buttonStartReceiving = view.findViewById(R.id.home_button_start_receiving); + buttonStartReceiving.setEnabled(!isRunning); + + Button buttonClearData = view.findViewById(R.id.home_button_clear_data); + buttonClearData.setEnabled(!isRunning); + Button buttonSaveDb = view.findViewById(R.id.home_button_export_db); + buttonSaveDb.setEnabled(!isRunning); + + Button buttonReportExposed = view.findViewById(R.id.home_button_report_exposed); + buttonReportExposed.setEnabled(!status.isReportedAsExposed()); + buttonReportExposed.setText(R.string.button_report_exposed); + buttonReportExposed.setOnClickListener( + v -> { + Calendar minCal = Calendar.getInstance(); + minCal.add(Calendar.DAY_OF_YEAR, EXPOSED_MIN_DATE_DIFF); + DialogFragment exposedDialog = + ExposedDialogFragment.newInstance(minCal.getTimeInMillis(), REGEX_VALIDITY_AUTH_CODE); + exposedDialog.setTargetFragment(this, REQUEST_CODE_REPORT_EXPOSED); + exposedDialog.show(getParentFragmentManager(), ExposedDialogFragment.class.getCanonicalName()); + }); + + EditText deanonymizationDeviceId = view.findViewById(R.id.deanonymization_device_id); + Switch deanonymizationSwitch = view.findViewById(R.id.deanonymization_switch); + if (DP3T.getCalibrationTestDeviceName(getContext()) != null) { + deanonymizationSwitch.setChecked(true); + deanonymizationDeviceId.setText(DP3T.getCalibrationTestDeviceName(getContext())); + } else { + deanonymizationSwitch.setChecked(false); + deanonymizationDeviceId.setText("0000"); + } + } + + private SpannableString formatStatusString(TracingStatus status) { + SpannableStringBuilder builder = new SpannableStringBuilder(); + boolean isTracking = status.isAdvertising() || status.isReceiving(); + builder.append(getString(isTracking ? R.string.status_tracking_active : R.string.status_tracking_inactive)).append("\n") + .setSpan(new StyleSpan(Typeface.BOLD), 0, builder.length() - 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + builder.append(getString(R.string.status_advertising, status.isAdvertising())).append("\n") + .append(getString(R.string.status_receiving, status.isReceiving())).append("\n"); + + long lastSyncDateUTC = status.getLastSyncDate(); + String lastSyncDateString = + lastSyncDateUTC > 0 ? DATE_FORMAT_SYNC.format(new Date(lastSyncDateUTC)) : "n/a"; + builder.append(getString(R.string.status_last_synced, lastSyncDateString)).append("\n") + .append(getString(R.string.status_self_exposed, status.isReportedAsExposed())).append("\n") + .append(getString(R.string.status_been_exposed, status.wasContactExposed())).append("\n") + .append(getString(R.string.status_number_handshakes, status.getNumberOfHandshakes())); + + ArrayList errors = status.getErrors(); + if (errors != null && errors.size() > 0) { + int start = builder.length(); + builder.append("\n"); + for (TracingStatus.ErrorState error : errors) { + builder.append("\n").append(error.toString()); + } + builder.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.red, null)), + start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + return new SpannableString(builder); + } + + private void sendExposedUpdate(Context context, Date onsetDate, String codeInputBase64) { + setExposeLoadingViewVisible(true); + + DP3T.sendIWasExposed(context, onsetDate, new ExposeeAuthData(codeInputBase64), new CallbackListener() { + @Override + public void onSuccess(Void response) { + DialogUtil.showMessageDialog(context, getString(R.string.dialog_title_success), + getString(R.string.dialog_message_request_success)); + setExposeLoadingViewVisible(false); + updateSdkStatus(); + } + + @Override + public void onError(Throwable throwable) { + DialogUtil.showMessageDialog(context, getString(R.string.dialog_title_error), + throwable.getLocalizedMessage()); + Log.e(TAG, throwable.getMessage(), throwable); + setExposeLoadingViewVisible(false); + } + }); + } + + private void setExposeLoadingViewVisible(boolean visible) { + View view = getView(); + if (view != null) { + view.findViewById(R.id.home_loading_view_exposed).setVisibility(visible ? View.VISIBLE : View.GONE); + view.findViewById(R.id.home_button_report_exposed).setVisibility(visible ? View.INVISIBLE : View.VISIBLE); + } + } + + private void setExportDbLoadingViewVisible(boolean visible) { + View view = getView(); + if (view != null) { + view.findViewById(R.id.home_loading_view_export_db).setVisibility(visible ? View.VISIBLE : View.GONE); + view.findViewById(R.id.home_button_export_db).setVisibility(visible ? View.INVISIBLE : View.VISIBLE); + } + } + +} diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/controls/ExposedDialogFragment.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/controls/ExposedDialogFragment.java new file mode 100644 index 00000000..6083f585 --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/controls/ExposedDialogFragment.java @@ -0,0 +1,136 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ + +package org.dpppt.android.calibration.controls; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.util.Base64; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; + +import java.nio.charset.StandardCharsets; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +import org.dpppt.android.calibration.R; +import org.dpppt.android.calibration.util.DatePickerFragmentDialog; + +public class ExposedDialogFragment extends DialogFragment { + + public static final String RESULT_EXTRA_DATE_MILLIS = "result_extra_date_millis"; + public static final String RESULT_EXTRA_AUTH_CODE_INPUT_BASE64 = "result_extra_auth_code_input_base64"; + + private static final String ARG_MIN_DATE = "arg_min_date"; + private static final String ARG_CODE_REGEX = "arg_code_regex"; + private static final int REQUEST_CODE_DATE_PICKER = 1; + + private static final DateFormat DATE_FORMAT = SimpleDateFormat.getDateInstance(); + + private long minDate; + private String authCodeRegex; + + private long selectedDate = -1; + + private EditText codeInputView; + private TextView errorView; + private EditText dateView; + + public static ExposedDialogFragment newInstance(long minDate, String authCodeValidityRegex) { + Bundle args = new Bundle(); + args.putLong(ARG_MIN_DATE, minDate); + args.putString(ARG_CODE_REGEX, authCodeValidityRegex); + ExposedDialogFragment fragment = new ExposedDialogFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + minDate = getArguments().getLong(ARG_MIN_DATE); + authCodeRegex = getArguments().getString(ARG_CODE_REGEX); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.dialog_fragment_exposed, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + dateView = view.findViewById(R.id.input_dialog_date); + dateView.setHint(DATE_FORMAT.format(new Date())); + dateView.setOnClickListener(v -> { + DialogFragment newFragment = DatePickerFragmentDialog + .newInstance(minDate, selectedDate > 0 ? selectedDate : Calendar.getInstance().getTimeInMillis()); + newFragment.setTargetFragment(this, REQUEST_CODE_DATE_PICKER); + newFragment.show(getParentFragmentManager(), DatePickerFragmentDialog.class.getCanonicalName()); + }); + codeInputView = view.findViewById(R.id.input_dialog_input); + errorView = view.findViewById(R.id.input_dialog_error_text); + + Button positiveButton = view.findViewById(R.id.input_dialog_positive_button); + positiveButton.setOnClickListener(v -> { + checkAndDismissDialog(selectedDate, codeInputView.getText().toString()); + }); + Button negativeButton = view.findViewById(R.id.input_dialog_negative_button); + negativeButton.setOnClickListener(v -> { + dismissDialog(); + }); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode == REQUEST_CODE_DATE_PICKER && resultCode == Activity.RESULT_OK) { + selectedDate = data.getLongExtra(DatePickerFragmentDialog.RESULT_EXTRA_DATE_MILLIS, -1); + dateView.setText(DATE_FORMAT.format(new Date(selectedDate))); + } + } + + private void checkAndDismissDialog(long selectedDate, String codeInput) { + boolean dateValid = selectedDate > minDate && selectedDate <= Calendar.getInstance().getTimeInMillis(); + if (!dateValid) { + errorView.setText(R.string.dialog_input_date_error); + errorView.setVisibility(View.VISIBLE); + return; + } + boolean codeValid = authCodeRegex == null || codeInput.matches(authCodeRegex); + if (!codeValid) { + errorView.setText(R.string.dialog_input_code_error); + errorView.setVisibility(View.VISIBLE); + return; + } + + Intent result = new Intent(); + result.putExtra(RESULT_EXTRA_DATE_MILLIS, selectedDate); + String inputBase64 = new String(Base64.encode(codeInput.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP), + StandardCharsets.UTF_8); + result.putExtra(RESULT_EXTRA_AUTH_CODE_INPUT_BASE64, inputBase64); + getTargetFragment().onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, result); + dismiss(); + } + + private void dismissDialog() { + getTargetFragment().onActivityResult(getTargetRequestCode(), Activity.RESULT_CANCELED, null); + dismiss(); + } + +} 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 new file mode 100644 index 00000000..548003b1 --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/handshakes/HandshakesFragment.java @@ -0,0 +1,159 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ + +package org.dpppt.android.calibration.handshakes; + +import android.os.Bundle; +import android.util.Base64; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Switch; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import java.text.SimpleDateFormat; +import java.util.*; + +import org.dpppt.android.sdk.internal.AppConfigManager; +import org.dpppt.android.sdk.internal.database.Database; +import org.dpppt.android.sdk.internal.database.models.Handshake; +import org.dpppt.android.calibration.R; + +public class HandshakesFragment extends Fragment { + + private static final int MAX_NUMBER_OF_MISSING_HANDSHAKES = 3; + + Switch rawHandshakeSwitch; + TextView handshakeList; + + public static HandshakesFragment newInstance() { + return new HandshakesFragment(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_handshakes, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + rawHandshakeSwitch = view.findViewById(R.id.raw_handshake_switch); + handshakeList = view.findViewById(R.id.handshake_list); + + loadHandshakes(rawHandshakeSwitch.isChecked()); + + rawHandshakeSwitch.setOnCheckedChangeListener((compoundButton, raw) -> loadHandshakes(raw)); + + view.findViewById(R.id.refresh).setOnClickListener((v) -> { + loadHandshakes(rawHandshakeSwitch.isChecked()); + }); + } + + private void loadHandshakes(boolean raw) { + handshakeList.setText("Loading..."); + new Database(getContext()).getHandshakes(response -> { + StringBuilder stringBuilder = new StringBuilder(); + SimpleDateFormat sdf = new SimpleDateFormat("dd.MM HH:mm:ss"); + if (raw) { + Collections.sort(response, (h1, h2) -> Long.compare(h2.getTimestamp(), h1.getTimestamp())); + for (Handshake handShake : response) { + stringBuilder.append(sdf.format(new Date(handShake.getTimestamp()))); + stringBuilder.append(" "); + stringBuilder.append(new String(Base64.encode(handShake.getEphId(), Base64.NO_WRAP)).substring(0, 10)); + stringBuilder.append("..."); + stringBuilder.append(" TxPowerLevel: "); + stringBuilder.append(handShake.getTxPowerLevel()); + stringBuilder.append(" RSSI:"); + stringBuilder.append(handShake.getRssi()); + stringBuilder.append("\n"); + } + } else { + for (HandshakeInterval interval : mergeHandshakes(response)) { + stringBuilder.append(sdf.format(new Date(interval.starttime))); + stringBuilder.append(" - "); + stringBuilder.append(sdf.format(new Date(interval.endtime))); + stringBuilder.append("\n"); + stringBuilder.append(interval.identifier); + stringBuilder.append(" Handshakes: "); + stringBuilder.append(interval.count); + stringBuilder.append(" / "); + stringBuilder.append(interval.expectedCount); + stringBuilder.append("\n\n"); + } + } + handshakeList.setText(stringBuilder.toString()); + }); + } + + private List mergeHandshakes(List handshakes) { + + HashMap> groupedHandshakes = new HashMap<>(); + for (Handshake handshake : handshakes) { + byte[] head = new byte[4]; + for (int i = 0; i < 4; i++) { + head[i] = handshake.getEphId()[i]; + } + String identifier = new String(head); + if (!groupedHandshakes.containsKey(identifier)) { + groupedHandshakes.put(identifier, new ArrayList<>()); + } + groupedHandshakes.get(identifier).add(handshake); + } + + long scanInterval = AppConfigManager.getInstance(getContext()).getScanInterval(); + List result = new ArrayList<>(); + for (Map.Entry> entry : groupedHandshakes.entrySet()) { + Collections.sort(entry.getValue(), (h1, h2) -> Long.compare(h1.getTimestamp(), h2.getTimestamp())); + int start = 0; + int end = 1; + while (end < entry.getValue().size()) { + if (entry.getValue().get(end).getTimestamp() - entry.getValue().get(end - 1).getTimestamp() > + MAX_NUMBER_OF_MISSING_HANDSHAKES * scanInterval) { + HandshakeInterval interval = new HandshakeInterval(); + interval.identifier = entry.getKey(); + interval.starttime = entry.getValue().get(start).getTimestamp(); + interval.endtime = entry.getValue().get(end - 1).getTimestamp(); + interval.count = end - start; + interval.expectedCount = + 1 + (int) Math.ceil((interval.endtime - interval.starttime) * 1.0 / scanInterval); + result.add(interval); + start = end; + } + end++; + } + + HandshakeInterval interval = new HandshakeInterval(); + interval.identifier = entry.getKey(); + interval.starttime = entry.getValue().get(start).getTimestamp(); + interval.endtime = entry.getValue().get(end - 1).getTimestamp(); + interval.count = end - start; + interval.expectedCount = + 1 + (int) Math.ceil((interval.endtime - interval.starttime) * 1.0 / scanInterval); + result.add(interval); + } + + Collections.sort(result, (i1, i2) -> Long.compare(i2.endtime, i1.endtime)); + + return result; + } + + private class HandshakeInterval { + String identifier; + long starttime; + long endtime; + int count; + int expectedCount; + + } + +} diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/logs/LogsAdapter.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/logs/LogsAdapter.java new file mode 100644 index 00000000..bd0aab5c --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/logs/LogsAdapter.java @@ -0,0 +1,137 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ + +package org.dpppt.android.calibration.logs; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.text.SimpleDateFormat; +import java.util.*; + +import org.dpppt.android.sdk.internal.logger.LogEntry; +import org.dpppt.android.sdk.internal.logger.LogLevel; +import org.dpppt.android.calibration.R; + +class LogsAdapter extends RecyclerView.Adapter { + + private final LayoutInflater inflater; + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.ENGLISH); + + private final List logs = new ArrayList<>(); + private List filteredLogs = new ArrayList<>(); + + private LogLevel filterLogLevel = LogLevel.DEBUG; + private final Set filterTags = new HashSet<>(); + + public LogsAdapter(Context context) { + inflater = LayoutInflater.from(context); + } + + @NonNull + @Override + public LogsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new LogsViewHolder(inflater.inflate(R.layout.view_log_entry, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull LogsViewHolder holder, int position) { + LogEntry logEntry = filteredLogs.get(position); + holder.timeView.setText(dateFormat.format(new Date(logEntry.getTime()))); + holder.levelView.setText(logEntry.getLevel().getKey()); + holder.tagView.setText(logEntry.getTag()); + holder.messageView.setText(logEntry.getMessage()); + + int color = 0x22000000; + switch (logEntry.getLevel()) { + case DEBUG: + color |= 0x0000FF; + break; + case INFO: + color |= 0x00AA00; + break; + case ERROR: + color |= 0xFF0000; + break; + } + holder.itemView.setBackgroundColor(color); + } + + @Override + public int getItemCount() { + return filteredLogs.size(); + } + + public long getLastLogTime() { + if (logs.isEmpty()) { + return -1; + } else { + return logs.get(logs.size() - 1).getTime(); + } + } + + public void appendLogs(List logEntries) { + logs.addAll(logEntries); + + int startIndex = filteredLogs.size(); + List filteredLogsToAdd = getFilteredLogs(logEntries); + filteredLogs.addAll(filteredLogsToAdd); + + notifyItemRangeInserted(startIndex, filteredLogsToAdd.size()); + } + + public void setFilterLogLevel(LogLevel logLevel) { + filterLogLevel = logLevel; + invalidateFilteredList(); + } + + public void setFilterTags(List tags) { + filterTags.clear(); + filterTags.addAll(tags); + filterTags.remove(""); + invalidateFilteredList(); + } + + private void invalidateFilteredList() { + filteredLogs = getFilteredLogs(logs); + notifyDataSetChanged(); + } + + private List getFilteredLogs(List logs) { + List filteredLogs = new ArrayList<>(); + for (LogEntry log : logs) { + if (log.getLevel().getI() >= filterLogLevel.getI()) { + if (filterTags.isEmpty() || filterTags.contains(log.getTag())) { + filteredLogs.add(log); + } + } + } + return filteredLogs; + } + +} + +class LogsViewHolder extends RecyclerView.ViewHolder { + + final TextView timeView; + final TextView levelView; + final TextView tagView; + final TextView messageView; + + LogsViewHolder(@NonNull View itemView) { + super(itemView); + timeView = itemView.findViewById(R.id.log_entry_time); + levelView = itemView.findViewById(R.id.log_entry_level); + tagView = itemView.findViewById(R.id.log_entry_tag); + messageView = itemView.findViewById(R.id.log_entry_msg); + } + +} diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/logs/LogsFragment.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/logs/LogsFragment.java new file mode 100644 index 00000000..7770cadd --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/logs/LogsFragment.java @@ -0,0 +1,127 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ + +package org.dpppt.android.calibration.logs; + +import android.os.Bundle; +import android.os.Handler; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.MultiAutoCompleteTextView; +import android.widget.Spinner; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.dpppt.android.sdk.internal.logger.LogEntry; +import org.dpppt.android.sdk.internal.logger.LogLevel; +import org.dpppt.android.sdk.internal.logger.Logger; +import org.dpppt.android.calibration.R; +import org.dpppt.android.calibration.util.OnTextChangedListener; + +public class LogsFragment extends Fragment { + + private RecyclerView logsList; + private LinearLayoutManager layoutManager; + private LogsAdapter logsAdapter; + + private Handler handler = new Handler(); + private Runnable updateLogsRunnable; + + private final List logLevels = Arrays.asList(LogLevel.DEBUG, LogLevel.INFO, LogLevel.ERROR); + + public static LogsFragment newInstance() { + return new LogsFragment(); + } + + public LogsFragment() { + super(R.layout.fragment_logs); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + logsList = view.findViewById(R.id.logs_list); + layoutManager = (LinearLayoutManager) logsList.getLayoutManager(); + logsAdapter = new LogsAdapter(getContext()); + logsList.setAdapter(logsAdapter); + + Spinner spinner = view.findViewById(R.id.logs_filter_level); + List logLevelValues = new ArrayList<>(logLevels.size()); + for (LogLevel logLevel : logLevels) { + logLevelValues.add(logLevel.getValue()); + } + ArrayAdapter logLevelSpinnerAdapter = + new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, logLevelValues); + logLevelSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(logLevelSpinnerAdapter); + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + logsAdapter.setFilterLogLevel(logLevels.get(position)); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + + MultiAutoCompleteTextView tagFilterInput = view.findViewById(R.id.logs_filter_tag); + + ArrayAdapter tagAutocompletionAdapter = + new ArrayAdapter<>(requireContext(), android.R.layout.simple_dropdown_item_1line, Logger.getTags()); + tagFilterInput.setAdapter(tagAutocompletionAdapter); + tagFilterInput.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer()); + tagFilterInput.addTextChangedListener(new OnTextChangedListener() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + String[] tags = tagFilterInput.getText().toString().split("\\s*,\\s*"); + logsAdapter.setFilterTags(Arrays.asList(tags)); + } + }); + + view.findViewById(R.id.logs_scrolltobottom).setOnClickListener(v -> { + if (logsAdapter.getItemCount() > 0) { + logsList.smoothScrollToPosition(logsAdapter.getItemCount() - 1); + } + }); + } + + @Override + public void onStart() { + super.onStart(); + + updateLogsRunnable = () -> { + boolean isAtBottom = layoutManager.findLastCompletelyVisibleItemPosition() == logsAdapter.getItemCount() - 1; + + List logs = Logger.getLogs(logsAdapter.getLastLogTime() + 1); + logsAdapter.appendLogs(logs); + + if (isAtBottom && logsAdapter.getItemCount() > 0) { + logsList.smoothScrollToPosition(logsAdapter.getItemCount() - 1); + } + + handler.postDelayed(updateLogsRunnable, 2 * 1000L); + }; + updateLogsRunnable.run(); + } + + @Override + public void onStop() { + super.onStop(); + + handler.removeCallbacks(updateLogsRunnable); + } + +} 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 new file mode 100644 index 00000000..140f3455 --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/parameters/ParametersFragment.java @@ -0,0 +1,214 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ + +package org.dpppt.android.calibration.parameters; + +import android.app.Activity; +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.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.SeekBar; +import android.widget.Spinner; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import org.dpppt.android.sdk.internal.AppConfigManager; +import org.dpppt.android.sdk.internal.BluetoothAdvertiseMode; +import org.dpppt.android.sdk.internal.BluetoothTxPowerLevel; +import org.dpppt.android.calibration.R; + +public class ParametersFragment extends Fragment { + + private static final int MIN_INTERVAL_SCANNING_SECONDS = 60; + private static final int MAX_INTERVAL_SCANNING_SECONDS = 900; + private static final int MIN_DURATION_SCANNING_SECONDS = 10; + private Spinner spinnerAdvertisingMode; + private Spinner spinnerPowerLevel; + private SeekBar seekBarScanInterval; + private SeekBar seekBarScanDuration; + private EditText inputScanInterval; + private EditText inputScanDuration; + + public static ParametersFragment newInstance() { + return new ParametersFragment(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_parameters, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + seekBarScanInterval = view.findViewById(R.id.parameter_seekbar_scan_interval); + inputScanInterval = view.findViewById(R.id.parameter_input_scan_interval); + seekBarScanDuration = view.findViewById(R.id.parameter_seekbar_scan_duration); + inputScanDuration = view.findViewById(R.id.parameter_input_scan_duration); + + seekBarScanInterval.setMax(MAX_INTERVAL_SCANNING_SECONDS - MIN_INTERVAL_SCANNING_SECONDS); + seekBarScanInterval.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + int intervalDuration = progress + MIN_INTERVAL_SCANNING_SECONDS; + inputScanInterval.setText(String.valueOf(intervalDuration)); + int newMaxProgress = intervalDuration - 1 - MIN_DURATION_SCANNING_SECONDS; + adjustNewDurationMaximum(newMaxProgress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + setScanInterval(seekBar.getProgress() + MIN_INTERVAL_SCANNING_SECONDS); + } + }); + inputScanInterval.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_DONE) { + String input = inputScanInterval.getText().toString(); + if (input.length() == 0) return true; + try { + int inputIntervalSeconds = Integer.parseInt(input); + inputIntervalSeconds = + Math.min(MAX_INTERVAL_SCANNING_SECONDS, Math.max(MIN_INTERVAL_SCANNING_SECONDS, inputIntervalSeconds)); + inputScanInterval.setText(String.valueOf(inputIntervalSeconds)); + seekBarScanInterval.setProgress(inputIntervalSeconds - MIN_INTERVAL_SCANNING_SECONDS); + setScanInterval(inputIntervalSeconds); + hideKeyboard(v); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + return true; + } + return false; + }); + + seekBarScanDuration.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + int scanDuration = progress + MIN_DURATION_SCANNING_SECONDS; + inputScanDuration.setText(String.valueOf(scanDuration)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + setScanDuration(seekBar.getProgress() + MIN_DURATION_SCANNING_SECONDS); + } + }); + inputScanDuration.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_DONE) { + String input = inputScanDuration.getText().toString(); + if (input.length() == 0) return true; + try { + int inputDurationSeconds = Integer.parseInt(input); + inputDurationSeconds = + Math.min(getScanInterval() - 1, Math.max(MIN_DURATION_SCANNING_SECONDS, inputDurationSeconds)); + inputScanDuration.setText(String.valueOf(inputDurationSeconds)); + seekBarScanDuration.setProgress(inputDurationSeconds - MIN_DURATION_SCANNING_SECONDS); + setScanDuration(inputDurationSeconds); + hideKeyboard(v); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + return true; + } + return false; + }); + + spinnerAdvertisingMode = view.findViewById(R.id.parameter_spinner_advertising_mode); + ArrayAdapter advertisingModeAdapter = + new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, BluetoothAdvertiseMode.values()); + spinnerAdvertisingMode.setAdapter(advertisingModeAdapter); + spinnerAdvertisingMode.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + setAdvertisingMode(BluetoothAdvertiseMode.values()[position]); + } + + @Override + public void onNothingSelected(AdapterView parent) { } + }); + + spinnerPowerLevel = view.findViewById(R.id.parameter_spinner_power_level); + ArrayAdapter powerLevelAdapter = + new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, BluetoothTxPowerLevel.values()); + spinnerPowerLevel.setAdapter(powerLevelAdapter); + spinnerPowerLevel.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + setAdvertPowerLevel(BluetoothTxPowerLevel.values()[position]); + } + + @Override + public void onNothingSelected(AdapterView parent) { } + }); + } + + private void adjustNewDurationMaximum(int durationProgressMaximum) { + int currentDurationProgress = seekBarScanDuration.getProgress(); + seekBarScanDuration.setMax(durationProgressMaximum); + if (currentDurationProgress > durationProgressMaximum) { + setScanDuration(durationProgressMaximum + MIN_DURATION_SCANNING_SECONDS); + } + } + + @Override + public void onResume() { + super.onResume(); + + AppConfigManager appConfigManager = AppConfigManager.getInstance(getContext()); + int interval = (int) (appConfigManager.getScanInterval() / 1000); + seekBarScanInterval.setProgress(interval - MIN_INTERVAL_SCANNING_SECONDS); + int duration = (int) (appConfigManager.getScanDuration() / 1000); + seekBarScanDuration.setProgress(duration - MIN_DURATION_SCANNING_SECONDS); + + BluetoothAdvertiseMode selectedMode = appConfigManager.getBluetoothAdvertiseMode(); + spinnerAdvertisingMode.setSelection(selectedMode.ordinal()); + + BluetoothTxPowerLevel selectedLevel = appConfigManager.getBluetoothTxPowerLevel(); + spinnerPowerLevel.setSelection(selectedLevel.ordinal()); + } + + private int getScanInterval() { + return seekBarScanInterval.getProgress() + MIN_INTERVAL_SCANNING_SECONDS; + } + + private void setScanInterval(int interval) { + AppConfigManager.getInstance(getContext()).setScanInterval(interval * 1000); + } + + private void setScanDuration(int duration) { + AppConfigManager.getInstance(getContext()).setScanDuration(duration * 1000); + } + + private void setAdvertPowerLevel(BluetoothTxPowerLevel powerLevel) { + AppConfigManager.getInstance(getContext()).setBluetoothPowerLevel(powerLevel); + } + + private void setAdvertisingMode(BluetoothAdvertiseMode mode) { + AppConfigManager.getInstance(getContext()).setBluetoothAdvertiseMode(mode); + } + + 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/java/org/dpppt/android/calibration/util/DatePickerFragmentDialog.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/DatePickerFragmentDialog.java new file mode 100644 index 00000000..a7998879 --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/DatePickerFragmentDialog.java @@ -0,0 +1,65 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ + +package org.dpppt.android.calibration.util; + +import android.app.Activity; +import android.app.DatePickerDialog; +import android.app.Dialog; +import android.content.Intent; +import android.os.Bundle; +import android.widget.DatePicker; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; + +import java.util.Calendar; + +public class DatePickerFragmentDialog extends DialogFragment implements DatePickerDialog.OnDateSetListener { + + public static final String RESULT_EXTRA_DATE_MILLIS = "result_extra_date_millis"; + + private static final String ARG_MIN_DATE = "arg_min_date"; + private static final String ARG_SELECTED_DATE = "arg_selected_date"; + + public static DatePickerFragmentDialog newInstance(long minDate, long selectedDate) { + Bundle args = new Bundle(); + args.putLong(ARG_MIN_DATE, minDate); + args.putLong(ARG_SELECTED_DATE, selectedDate); + DatePickerFragmentDialog fragment = new DatePickerFragmentDialog(); + fragment.setArguments(args); + return fragment; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + + final Calendar c = Calendar.getInstance(); + long now = c.getTimeInMillis(); + + c.setTimeInMillis(getArguments().getLong(ARG_SELECTED_DATE, 0)); + + int selectedDay = c.get(Calendar.DAY_OF_MONTH); + int selectedMonth = c.get(Calendar.MONTH); + int selectedYear = c.get(Calendar.YEAR); + DatePickerDialog dialog = new DatePickerDialog(getActivity(), this, selectedYear, selectedMonth, selectedDay); + + dialog.getDatePicker().setMinDate(getArguments().getLong(ARG_MIN_DATE, c.getTimeInMillis())); + dialog.getDatePicker().setMaxDate(now); + return dialog; + } + + @Override + public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) { + Calendar selectedDate = Calendar.getInstance(); + selectedDate.set(year, month, dayOfMonth); + Intent result = new Intent(); + result.putExtra(RESULT_EXTRA_DATE_MILLIS, selectedDate.getTimeInMillis()); + getTargetFragment().onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, result); + } + +} diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/DialogUtil.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/DialogUtil.java new file mode 100644 index 00000000..256b9ef8 --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/DialogUtil.java @@ -0,0 +1,40 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ + +package org.dpppt.android.calibration.util; + +import android.content.Context; +import android.content.DialogInterface; + +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; + +import org.dpppt.android.calibration.R; + +public class DialogUtil { + + public static void showConfirmDialog(Context context, @StringRes int title, + DialogInterface.OnClickListener positiveClickListener) { + new AlertDialog.Builder(context) + .setTitle(title) + .setMessage(R.string.dialog_confirm_message) + .setPositiveButton(R.string.dialog_confirm_positive_button, (dialog, which) -> { + dialog.dismiss(); + positiveClickListener.onClick(dialog, which); + }) + .setNegativeButton(R.string.dialog_confirm_negative_button, (dialog, which) -> dialog.dismiss()) + .show(); + } + + public static void showMessageDialog(Context context, String title, String msg) { + new AlertDialog.Builder(context) + .setTitle(title) + .setMessage(msg) + .setPositiveButton(R.string.dialog_button_ok, (dialog, which) -> dialog.dismiss()) + .show(); + } + +} diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/NotificationUtil.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/NotificationUtil.java new file mode 100644 index 00000000..3a96a89b --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/NotificationUtil.java @@ -0,0 +1,56 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ +package org.dpppt.android.calibration.util; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import androidx.annotation.DrawableRes; +import androidx.annotation.RequiresApi; +import androidx.annotation.StringRes; +import androidx.core.app.NotificationCompat; + +import org.dpppt.android.calibration.R; + +public class NotificationUtil { + + private static final String NOTIFICATION_CHANNEL_ID = "dp3t_sdk_sample_channel"; + private static final int NOTIFICATION_ID = 1; + + @RequiresApi(api = Build.VERSION_CODES.O) + public static void createNotificationChannel(Context context) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + String channelName = context.getString(R.string.app_name); + NotificationChannel channel = + new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_HIGH); + channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + notificationManager.createNotificationChannel(channel); + } + + public static void showNotification(Context context, @StringRes int title, @StringRes int message, @DrawableRes int icon) { + Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); + PendingIntent contentIntent = null; + if (launchIntent != null) { + contentIntent = PendingIntent.getActivity(context, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + Notification notification = + new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) + .setContentTitle(context.getString(title)) + .setContentText(context.getString(message)) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setSmallIcon(icon) + .setContentIntent(contentIntent) + .build(); + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(NOTIFICATION_ID, notification); + } + +} diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/OnTextChangedListener.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/OnTextChangedListener.java new file mode 100644 index 00000000..653acd10 --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/OnTextChangedListener.java @@ -0,0 +1,21 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ + +package org.dpppt.android.calibration.util; + +import android.text.Editable; +import android.text.TextWatcher; + +public abstract class OnTextChangedListener implements TextWatcher { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void afterTextChanged(Editable s) { + } + +} diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/PreferencesUtil.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/PreferencesUtil.java new file mode 100644 index 00000000..700bb6b3 --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/PreferencesUtil.java @@ -0,0 +1,26 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ +package org.dpppt.android.calibration.util; + +import android.content.Context; +import android.content.SharedPreferences; + +public class PreferencesUtil { + + private static final String PREFS_DP3T_SDK_SAMPLE = "preferences_dp3t_sdk_sample"; + private static final String PREF_KEY_EXPOSED_NOTIFICATION = "pref_key_exposed_notification"; + + public static boolean isExposedNotificationShown(Context context) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_DP3T_SDK_SAMPLE, Context.MODE_PRIVATE); + return prefs.getBoolean(PREF_KEY_EXPOSED_NOTIFICATION, false); + } + + public static void setExposedNotificationShown(Context context) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_DP3T_SDK_SAMPLE, Context.MODE_PRIVATE); + prefs.edit().putBoolean(PREF_KEY_EXPOSED_NOTIFICATION, true).apply(); + } + +} diff --git a/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/RequirementsUtil.java b/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/RequirementsUtil.java new file mode 100644 index 00000000..75485469 --- /dev/null +++ b/calibration-app/app/src/main/java/org/dpppt/android/calibration/util/RequirementsUtil.java @@ -0,0 +1,36 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2020. All rights reserved. + */ + +package org.dpppt.android.calibration.util; + +import android.Manifest; +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.PowerManager; +import androidx.core.content.ContextCompat; + +public class RequirementsUtil { + + public static boolean isLocationPermissionGranted(Context context) { + return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED; + } + + public static boolean isBatteryOptimizationDeactivated(Context context) { + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return powerManager.isIgnoringBatteryOptimizations(context.getPackageName()); + } + + public static boolean isBluetoothEnabled() { + BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { + return false; + } + return true; + } + +} diff --git a/calibration-app/app/src/main/res/color/selector_bottom_navigation_item_color.xml b/calibration-app/app/src/main/res/color/selector_bottom_navigation_item_color.xml new file mode 100644 index 00000000..d2630f0c --- /dev/null +++ b/calibration-app/app/src/main/res/color/selector_bottom_navigation_item_color.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/calibration-app/app/src/main/res/color/selector_button_red.xml b/calibration-app/app/src/main/res/color/selector_button_red.xml new file mode 100644 index 00000000..554b0498 --- /dev/null +++ b/calibration-app/app/src/main/res/color/selector_button_red.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/calibration-app/app/src/main/res/color/selector_default_button_color.xml b/calibration-app/app/src/main/res/color/selector_default_button_color.xml new file mode 100644 index 00000000..b89514b1 --- /dev/null +++ b/calibration-app/app/src/main/res/color/selector_default_button_color.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/calibration-app/app/src/main/res/color/selector_requirements_button_color.xml b/calibration-app/app/src/main/res/color/selector_requirements_button_color.xml new file mode 100644 index 00000000..1f944634 --- /dev/null +++ b/calibration-app/app/src/main/res/color/selector_requirements_button_color.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/calibration-app/app/src/main/res/color/selector_requirements_button_icon_color.xml b/calibration-app/app/src/main/res/color/selector_requirements_button_icon_color.xml new file mode 100644 index 00000000..c42470c0 --- /dev/null +++ b/calibration-app/app/src/main/res/color/selector_requirements_button_icon_color.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/calibration-app/app/src/main/res/color/selector_requirements_text_color.xml b/calibration-app/app/src/main/res/color/selector_requirements_text_color.xml new file mode 100644 index 00000000..32d0cacd --- /dev/null +++ b/calibration-app/app/src/main/res/color/selector_requirements_text_color.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/calibration-app/app/src/main/res/color/selector_tracking_button_color.xml b/calibration-app/app/src/main/res/color/selector_tracking_button_color.xml new file mode 100644 index 00000000..923a8479 --- /dev/null +++ b/calibration-app/app/src/main/res/color/selector_tracking_button_color.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/calibration-app/app/src/main/res/color/selector_tracking_text_color.xml b/calibration-app/app/src/main/res/color/selector_tracking_text_color.xml new file mode 100644 index 00000000..2e791630 --- /dev/null +++ b/calibration-app/app/src/main/res/color/selector_tracking_text_color.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/calibration-app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/calibration-app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..df78d839 --- /dev/null +++ b/calibration-app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/calibration-app/app/src/main/res/drawable/ic_check_circle.xml b/calibration-app/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 00000000..3b00e973 --- /dev/null +++ b/calibration-app/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/calibration-app/app/src/main/res/drawable/ic_controls.xml b/calibration-app/app/src/main/res/drawable/ic_controls.xml new file mode 100644 index 00000000..f537c744 --- /dev/null +++ b/calibration-app/app/src/main/res/drawable/ic_controls.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/calibration-app/app/src/main/res/drawable/ic_error.xml b/calibration-app/app/src/main/res/drawable/ic_error.xml new file mode 100644 index 00000000..f152420a --- /dev/null +++ b/calibration-app/app/src/main/res/drawable/ic_error.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/calibration-app/app/src/main/res/drawable/ic_handshakes.xml b/calibration-app/app/src/main/res/drawable/ic_handshakes.xml new file mode 100644 index 00000000..66af26ff --- /dev/null +++ b/calibration-app/app/src/main/res/drawable/ic_handshakes.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/calibration-app/app/src/main/res/drawable/ic_launcher_background.xml b/calibration-app/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..ccdf9319 --- /dev/null +++ b/calibration-app/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/calibration-app/app/src/main/res/drawable/ic_logs.xml b/calibration-app/app/src/main/res/drawable/ic_logs.xml new file mode 100644 index 00000000..06edfff8 --- /dev/null +++ b/calibration-app/app/src/main/res/drawable/ic_logs.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/calibration-app/app/src/main/res/drawable/ic_parameters.xml b/calibration-app/app/src/main/res/drawable/ic_parameters.xml new file mode 100644 index 00000000..1edace9f --- /dev/null +++ b/calibration-app/app/src/main/res/drawable/ic_parameters.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/calibration-app/app/src/main/res/drawable/ic_refresh.xml b/calibration-app/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 00000000..0b73f643 --- /dev/null +++ b/calibration-app/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/calibration-app/app/src/main/res/drawable/ic_to_bottom.xml b/calibration-app/app/src/main/res/drawable/ic_to_bottom.xml new file mode 100644 index 00000000..bff622da --- /dev/null +++ b/calibration-app/app/src/main/res/drawable/ic_to_bottom.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/calibration-app/app/src/main/res/drawable/selector_requirements_button_icon.xml b/calibration-app/app/src/main/res/drawable/selector_requirements_button_icon.xml new file mode 100644 index 00000000..fb151435 --- /dev/null +++ b/calibration-app/app/src/main/res/drawable/selector_requirements_button_icon.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/calibration-app/app/src/main/res/layout/activity_main.xml b/calibration-app/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..e78a0514 --- /dev/null +++ b/calibration-app/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,26 @@ + + + + + + + + + + \ No newline at end of file diff --git a/calibration-app/app/src/main/res/layout/dialog_fragment_exposed.xml b/calibration-app/app/src/main/res/layout/dialog_fragment_exposed.xml new file mode 100644 index 00000000..b50ad133 --- /dev/null +++ b/calibration-app/app/src/main/res/layout/dialog_fragment_exposed.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + +