From 26ca20fc3daeccc8f4a63d8097c4c3bb99aa27cc Mon Sep 17 00:00:00 2001 From: COVIDSafe Support <64945427+covidsafe-support@users.noreply.github.com> Date: Sat, 19 Dec 2020 16:14:54 +1100 Subject: [PATCH] COVIDSafe code from version 2.0 (#38) --- app/build.gradle | 14 +- .../2.json | 114 --- .../2.json | 114 --- .../sensor/ble/filter/AdvertParserTests.java | 166 +++ .../ble/filter/BLEDeviceFilterTests.java | 245 +++++ .../sensor/datatype/PayloadDataTests.java | 21 + .../datatype/PseudoDeviceAddressTests.java | 61 ++ app/src/main/AndroidManifest.xml | 5 + .../au/gov/health/covidsafe/HomeActivity.kt | 33 +- .../au/gov/health/covidsafe/app/TracerApp.kt | 26 +- .../covidsafe/bluetooth/gatt/GattServer.kt | 2 +- .../extensions/PermissionExtensions.kt | 2 +- .../notifications/NotificationTemplates.kt | 2 - .../sensor/DefaultSensorDelegate.java | 55 + .../gov/health/covidsafe/sensor/Sensor.java | 17 + .../health/covidsafe/sensor/SensorArray.java | 93 ++ .../covidsafe/sensor/SensorDelegate.java | 44 + .../covidsafe/sensor/ble/BLEDatabase.java | 38 + .../sensor/ble/BLEDatabaseDelegate.java | 14 + .../covidsafe/sensor/ble/BLEDevice.java | 330 ++++++ .../sensor/ble/BLEDeviceAttribute.java | 9 + .../sensor/ble/BLEDeviceDelegate.java | 10 + .../sensor/ble/BLEDeviceOperatingSystem.java | 9 + .../covidsafe/sensor/ble/BLEDeviceState.java | 10 + .../covidsafe/sensor/ble/BLEReceiver.java | 18 + .../covidsafe/sensor/ble/BLESensor.java | 10 + .../sensor/ble/BLESensorConfiguration.java | 64 ++ .../health/covidsafe/sensor/ble/BLETimer.java | 123 +++ .../sensor/ble/BLETimerDelegate.java | 10 + .../covidsafe/sensor/ble/BLETransmitter.java | 39 + .../covidsafe/sensor/ble/BLE_TxPower.java | 13 + .../sensor/ble/BluetoothStateManager.java | 16 + .../ble/BluetoothStateManagerDelegate.java | 11 + .../sensor/ble/ConcreteBLEDatabase.java | 350 +++++++ .../sensor/ble/ConcreteBLEReceiver.kt | 961 ++++++++++++++++++ .../sensor/ble/ConcreteBLESensor.java | 194 ++++ .../sensor/ble/ConcreteBLETransmitter.kt | 610 +++++++++++ .../ble/ConcreteBluetoothStateManager.java | 86 ++ .../BLEAdvertAppleManufacturerSegment.java | 26 + .../ble/filter/BLEAdvertManufacturerData.java | 28 + .../sensor/ble/filter/BLEAdvertParser.java | 162 +++ .../sensor/ble/filter/BLEAdvertSegment.java | 31 + .../ble/filter/BLEAdvertSegmentType.java | 66 ++ .../sensor/ble/filter/BLEDeviceFilter.java | 196 ++++ .../ble/filter/BLEScanResponseData.java | 22 + .../covidsafe/sensor/data/BatteryLog.java | 67 ++ .../sensor/data/ConcreteSensorLogger.java | 148 +++ .../covidsafe/sensor/data/ContactLog.java | 69 ++ .../covidsafe/sensor/data/DetectionLog.java | 87 ++ .../covidsafe/sensor/data/SensorLogger.java | 14 + .../sensor/data/SensorLoggerLevel.java | 9 + .../covidsafe/sensor/data/StatisticsLog.java | 116 +++ .../covidsafe/sensor/data/TextFile.java | 91 ++ .../covidsafe/sensor/datatype/Base64.java | 96 ++ .../sensor/datatype/BluetoothState.java | 9 + .../covidsafe/sensor/datatype/Callback.java | 10 + .../covidsafe/sensor/datatype/Data.java | 117 +++ .../covidsafe/sensor/datatype/Location.java | 26 + .../sensor/datatype/LocationReference.java | 10 + .../sensor/datatype/PayloadData.java | 41 + .../sensor/datatype/PayloadSharingData.java | 21 + .../sensor/datatype/PayloadTimestamp.java | 20 + .../datatype/PlacenameLocationReference.java | 18 + .../covidsafe/sensor/datatype/Proximity.java | 31 + .../datatype/ProximityMeasurementUnit.java | 13 + .../sensor/datatype/PseudoDeviceAddress.java | 148 +++ .../covidsafe/sensor/datatype/RSSI.java | 34 + .../covidsafe/sensor/datatype/Sample.java | 74 ++ .../sensor/datatype/SensorState.java | 15 + .../covidsafe/sensor/datatype/SensorType.java | 17 + .../datatype/SignalCharacteristicData.java | 152 +++ .../SignalCharacteristicDataType.java | 9 + .../sensor/datatype/TargetIdentifier.java | 44 + .../sensor/datatype/TimeInterval.java | 37 + .../covidsafe/sensor/datatype/Triple.java | 26 + .../covidsafe/sensor/datatype/Tuple.java | 27 + .../covidsafe/sensor/datatype/UInt8.java | 23 + .../WGS84CircularAreaLocationReference.java | 21 + .../datatype/WGS84PointLocationReference.java | 20 + .../payload/DefaultPayloadDataSupplier.java | 33 + .../sensor/payload/PayloadDataSupplier.java | 20 + .../sensor/service/ForegroundService.java | 46 + .../sensor/service/NotificationService.java | 81 ++ .../services/BluetoothMonitoringService.kt | 535 ++++------ .../IBluetoothGattInvocationHandler.java | 2 +- .../streetpass/StreetPassPairingFix.kt | 28 +- .../covidsafe/streetpass/StreetPassServer.kt | 32 - .../covidsafe/streetpass/StreetPassWorker.kt | 4 +- .../persistence/StreetPassRecordStorage.kt | 1 - .../health/covidsafe/ui/home/HomeFragment.kt | 10 +- .../ui/home/HomeFragmentViewModel.kt | 4 + .../fragment/permission/PermissionFragment.kt | 26 +- app/src/main/res/layout/fragment_home.xml | 82 +- app/src/main/res/values-ar/strings.xml | 1 - app/src/main/res/values-el-rGR/strings.xml | 1 - app/src/main/res/values-it-rIT/strings.xml | 1 - app/src/main/res/values-ko/strings.xml | 1 - app/src/main/res/values-pa-rIN/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-vi/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - app/src/main/res/values/strings.xml | 13 +- .../module/feedback/FeedbackModule.java | 1 + gradle.properties | 8 +- 105 files changed, 6415 insertions(+), 651 deletions(-) delete mode 100644 app/schemas/au.gov.dta.covidsafe.streetpass.persistence.StreetPassRecordDatabase/2.json delete mode 100644 app/schemas/au.gov.dta.covidtrace.streetpass.persistence.StreetPassRecordDatabase/2.json create mode 100644 app/src/androidTest/java/au/gov/health/covidsafe/sensor/ble/filter/AdvertParserTests.java create mode 100644 app/src/androidTest/java/au/gov/health/covidsafe/sensor/ble/filter/BLEDeviceFilterTests.java create mode 100644 app/src/androidTest/java/au/gov/health/covidsafe/sensor/datatype/PayloadDataTests.java create mode 100644 app/src/androidTest/java/au/gov/health/covidsafe/sensor/datatype/PseudoDeviceAddressTests.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/DefaultSensorDelegate.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/Sensor.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/SensorArray.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/SensorDelegate.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDatabase.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDatabaseDelegate.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDevice.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceAttribute.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceDelegate.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceOperatingSystem.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceState.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEReceiver.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLESensor.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLESensorConfiguration.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLETimer.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLETimerDelegate.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLETransmitter.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLE_TxPower.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BluetoothStateManager.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/BluetoothStateManagerDelegate.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLEDatabase.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLEReceiver.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLESensor.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLETransmitter.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBluetoothStateManager.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertAppleManufacturerSegment.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertManufacturerData.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertParser.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertSegment.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertSegmentType.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEDeviceFilter.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEScanResponseData.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/data/BatteryLog.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/data/ConcreteSensorLogger.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/data/ContactLog.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/data/DetectionLog.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/data/SensorLogger.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/data/SensorLoggerLevel.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/data/StatisticsLog.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/data/TextFile.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Base64.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/BluetoothState.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Callback.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Data.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Location.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/LocationReference.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PayloadData.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PayloadSharingData.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PayloadTimestamp.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PlacenameLocationReference.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Proximity.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/ProximityMeasurementUnit.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PseudoDeviceAddress.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/RSSI.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Sample.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SensorState.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SensorType.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SignalCharacteristicData.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SignalCharacteristicDataType.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/TargetIdentifier.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/TimeInterval.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Triple.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Tuple.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/UInt8.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/WGS84CircularAreaLocationReference.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/datatype/WGS84PointLocationReference.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/payload/DefaultPayloadDataSupplier.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/payload/PayloadDataSupplier.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/service/ForegroundService.java create mode 100644 app/src/main/java/au/gov/health/covidsafe/sensor/service/NotificationService.java delete mode 100644 app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassServer.kt diff --git a/app/build.gradle b/app/build.gradle index 6cf02ff..bc01b32 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,8 +29,8 @@ android { applicationId "au.gov.health.covidsafe" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 80 - versionName "1.14.0" + versionCode 94 + versionName "2.0" buildConfigField "String", "GITHASH", "\"${getGitHash()}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -62,6 +62,7 @@ android { buildConfigField "long", "HEALTH_CHECK_INTERVAL", HEALTH_CHECK_INTERVAL buildConfigField "long", "CONNECTION_TIMEOUT", CONNECTION_TIMEOUT buildConfigField "long", "BLACKLIST_DURATION", BLACKLIST_DURATION + buildConfigField "long", "PERIPHERAL_PAYLOAD_SAVE_INTERVAL", PERIPHERAL_PAYLOAD_SAVE_INTERVAL buildConfigField "long", "ADVERTISING_DURATION", ADVERTISING_DURATION buildConfigField "long", "ADVERTISING_INTERVAL", ADVERTISING_INTERVAL @@ -84,6 +85,9 @@ android { buildTypes { debug { buildConfigField "String", "BLE_SSID", STAGING_SERVICE_UUID + buildConfigField "String", "BLE_ANDROIDSIGNALCHARACTERISTIC", STAGING_ANDROIDSIGNALCHARACTERISTICUUID + buildConfigField "String", "BLE_IOSSIGNALCHARACTERISTIC", STAGING_IOSSIGNALCHARACTERISTICUUID + buildConfigField "String", "BLE_PAYLOADCHARACTERISTIC", SRAGING_PAYLOADCHARACTERISTICUUID buildConfigField "boolean", "ENABLE_DEBUG_SCREEN", "true" buildConfigField "String", "END_POINT_PREFIX", TEST_END_POINT_PREFIX buildConfigField "String", "BASE_URL", TEST_BASE_URL @@ -100,6 +104,9 @@ android { staging { buildConfigField "String", "BLE_SSID", STAGING_SERVICE_UUID + buildConfigField "String", "BLE_ANDROIDSIGNALCHARACTERISTIC", STAGING_ANDROIDSIGNALCHARACTERISTICUUID + buildConfigField "String", "BLE_IOSSIGNALCHARACTERISTIC", STAGING_IOSSIGNALCHARACTERISTICUUID + buildConfigField "String", "BLE_PAYLOADCHARACTERISTIC", SRAGING_PAYLOADCHARACTERISTICUUID buildConfigField "boolean", "ENABLE_DEBUG_SCREEN", "true" buildConfigField "String", "END_POINT_PREFIX", STAGING_END_POINT_PREFIX buildConfigField "String", "BASE_URL", STAGING_BASE_URL @@ -125,6 +132,9 @@ android { release { buildConfigField "String", "BLE_SSID", PRODUCTION_SERVICE_UUID + buildConfigField "String", "BLE_ANDROIDSIGNALCHARACTERISTIC", PRODUCTION_ANDROIDSIGNALCHARACTERISTICUUID + buildConfigField "String", "BLE_IOSSIGNALCHARACTERISTIC", PRODUCTION_IOSSIGNALCHARACTERISTICUUID + buildConfigField "String", "BLE_PAYLOADCHARACTERISTIC", PRODUCTION_PAYLOADCHARACTERISTICUUID buildConfigField "String", "END_POINT_PREFIX", PRODUCTION_END_POINT_PREFIX buildConfigField "String", "BASE_URL", PROD_BASE_URL buildConfigField "String", "IOS_BACKGROUND_UUID", PRODUCTION_BACKGROUND_IOS_SERVICE_UUID diff --git a/app/schemas/au.gov.dta.covidsafe.streetpass.persistence.StreetPassRecordDatabase/2.json b/app/schemas/au.gov.dta.covidsafe.streetpass.persistence.StreetPassRecordDatabase/2.json deleted file mode 100644 index 3315850..0000000 --- a/app/schemas/au.gov.dta.covidsafe.streetpass.persistence.StreetPassRecordDatabase/2.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 2, - "identityHash": "9a95fc8ad88c160bf76c0ba4747db316", - "entities": [ - { - "tableName": "record_table", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `v` INTEGER NOT NULL, `msg` TEXT NOT NULL, `org` TEXT NOT NULL, `modelP` TEXT NOT NULL, `modelC` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `txPower` INTEGER)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "v", - "columnName": "v", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "msg", - "columnName": "msg", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "org", - "columnName": "org", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "modelP", - "columnName": "modelP", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "modelC", - "columnName": "modelC", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rssi", - "columnName": "rssi", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "txPower", - "columnName": "txPower", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "status_table", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `msg` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "msg", - "columnName": "msg", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9a95fc8ad88c160bf76c0ba4747db316')" - ] - } -} \ No newline at end of file diff --git a/app/schemas/au.gov.dta.covidtrace.streetpass.persistence.StreetPassRecordDatabase/2.json b/app/schemas/au.gov.dta.covidtrace.streetpass.persistence.StreetPassRecordDatabase/2.json deleted file mode 100644 index 3315850..0000000 --- a/app/schemas/au.gov.dta.covidtrace.streetpass.persistence.StreetPassRecordDatabase/2.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 2, - "identityHash": "9a95fc8ad88c160bf76c0ba4747db316", - "entities": [ - { - "tableName": "record_table", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `v` INTEGER NOT NULL, `msg` TEXT NOT NULL, `org` TEXT NOT NULL, `modelP` TEXT NOT NULL, `modelC` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `txPower` INTEGER)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "v", - "columnName": "v", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "msg", - "columnName": "msg", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "org", - "columnName": "org", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "modelP", - "columnName": "modelP", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "modelC", - "columnName": "modelC", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rssi", - "columnName": "rssi", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "txPower", - "columnName": "txPower", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "status_table", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `msg` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "msg", - "columnName": "msg", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9a95fc8ad88c160bf76c0ba4747db316')" - ] - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/au/gov/health/covidsafe/sensor/ble/filter/AdvertParserTests.java b/app/src/androidTest/java/au/gov/health/covidsafe/sensor/ble/filter/AdvertParserTests.java new file mode 100644 index 0000000..9ef7e7f --- /dev/null +++ b/app/src/androidTest/java/au/gov/health/covidsafe/sensor/ble/filter/AdvertParserTests.java @@ -0,0 +1,166 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble.filter; + +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class AdvertParserTests { + + // MARK: Low level individual parsing functions + + @Test + public void testDataSubsetBigEndian() throws Exception { + byte[] data = new byte[]{0,1,5,6,7,8,12,13,14}; + assertEquals(5, data[2]); + assertEquals(6, data[3]); + assertEquals(7, data[4]); + assertEquals(8, data[5]); + byte[] result = BLEAdvertParser.subDataBigEndian(data,2,4); + assertNotNull(result); + assertEquals(4, result.length); + assertEquals(5, result[0]); + assertEquals(6, result[1]); + assertEquals(7, result[2]); + assertEquals(8, result[3]); + } + + @Test + public void testDataSubsetLittleEndian() throws Exception { + byte[] data = new byte[]{0,1,5,6,7,8,12,13,14}; + byte[] result = BLEAdvertParser.subDataLittleEndian(data,2,4); + assertNotNull(result); + assertEquals(4, result.length); + assertEquals(8, result[0]); + assertEquals(7, result[1]); + assertEquals(6, result[2]); + assertEquals(5, result[3]); + } + + @Test + public void testDataSubsetBigEndianOverflow() throws Exception { + byte[] data = new byte[]{0,1,5,6,7}; + byte[] result = BLEAdvertParser.subDataBigEndian(data,2,4); + assertNotNull(result); + assertEquals(0, result.length); + } + + @Test + public void testDataSubsetLittleEndianOverflow() throws Exception { + byte[] data = new byte[]{0, 1, 5, 6, 7}; + byte[] result = BLEAdvertParser.subDataLittleEndian(data, 2, 4); + assertNotNull(result); + assertEquals(0, result.length); + } + + @Test + public void testDataSubsetBigEndianLowIndex() throws Exception { + byte[] data = new byte[]{0,1,5,6,7}; + byte[] result = BLEAdvertParser.subDataBigEndian(data,-1,4); + assertNotNull(result); + assertEquals(0, result.length); + } + + @Test + public void testDataSubsetLittleEndianLowIndex() throws Exception { + byte[] data = new byte[]{0,1,5,6,7}; + byte[] result = BLEAdvertParser.subDataLittleEndian(data,-1,4); + assertNotNull(result); + assertEquals(0, result.length); + } + + @Test + public void testDataSubsetBigEndianHighIndex() throws Exception { + byte[] data = new byte[]{0,1,5,6,7}; + byte[] result = BLEAdvertParser.subDataBigEndian(data,5,4); + assertNotNull(result); + assertEquals(0, result.length); + } + + @Test + public void testDataSubsetLittleEndianHighIndex() throws Exception { + byte[] data = new byte[]{0,1,5,6,7}; + byte[] result = BLEAdvertParser.subDataLittleEndian(data,5,4); + assertNotNull(result); + assertEquals(0, result.length); + } + + @Test + public void testDataSubsetBigEndianLargeLength() throws Exception { + byte[] data = new byte[]{0,1,5,6,7}; + byte[] result = BLEAdvertParser.subDataBigEndian(data,2,4); + assertNotNull(result); + assertEquals(0, result.length); + } + + @Test + public void testDataSubsetLittleEndianLargeLength() throws Exception { + byte[] data = new byte[]{0,1,5,6,7}; + byte[] result = BLEAdvertParser.subDataLittleEndian(data,2,4); + assertNotNull(result); + assertEquals(0, result.length); + } + + @Test + public void testDataSubsetBigEndianEmptyData() throws Exception { + byte[] data = new byte[]{}; + byte[] result = BLEAdvertParser.subDataBigEndian(data,0,1); + assertNotNull(result); + assertEquals(0, result.length); + } + + @Test + public void testDataSubsetLittleEndianEmptyData() throws Exception { + byte[] data = new byte[]{}; + byte[] result = BLEAdvertParser.subDataLittleEndian(data,0,1); + assertNotNull(result); + assertEquals(0, result.length); + } + + @Test + public void testDataSubsetBigEndianNullData() throws Exception { + byte[] result = BLEAdvertParser.subDataBigEndian(null,0,1); + assertNotNull(result); + assertEquals(0, result.length); + } + + @Test + public void testDataSubsetLittleNullEmptyData() throws Exception { + byte[] result = BLEAdvertParser.subDataLittleEndian(null,0,1); + assertNotNull(result); + assertEquals(0, result.length); + } + + // MARK: HIGH LEVEL FULL PACKET METHODS + + @Test + public void testAppleTVFG() throws Exception { + byte[] data = new byte[]{2, 1, 26, 2, 10, 8, + (byte)0x0c, (byte)0xff, (byte)0x4c, (byte)0x00, + (byte)0x10, (byte)0x07, (byte)0x33, + (byte)0x1f, (byte)0x2c, (byte)0x30, (byte)0x2f, (byte)0x92, + (byte)0x58 + }; + assertEquals("02011a020a080cff4c001007331f2c302f9258", BLEAdvertParser.hex(data)); + BLEScanResponseData result = BLEAdvertParser.parseScanResponse(data,0); + assertNotNull(result); + + assertEquals(3,result.segments.size()); + List manu = BLEAdvertParser.extractManufacturerData(result.segments); + assertNotNull(manu); + assertEquals(1, manu.size()); + + byte[] manuData = manu.get(0).data; + assertNotNull(manuData); + assertEquals(9, manuData.length); + assertEquals(16, manuData[0]); // int 16 = byte 10 + assertEquals(7, manuData[1]); // int 7 = byte 07 + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/au/gov/health/covidsafe/sensor/ble/filter/BLEDeviceFilterTests.java b/app/src/androidTest/java/au/gov/health/covidsafe/sensor/ble/filter/BLEDeviceFilterTests.java new file mode 100644 index 0000000..6801749 --- /dev/null +++ b/app/src/androidTest/java/au/gov/health/covidsafe/sensor/ble/filter/BLEDeviceFilterTests.java @@ -0,0 +1,245 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble.filter; + +import au.gov.health.covidsafe.sensor.datatype.Data; + +import org.junit.Test; + +import java.util.List; +import java.util.Random; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class BLEDeviceFilterTests { + + @Test + public void testHexTransform() throws Exception { + final Random random = new Random(0); + for (int i = 0; i < 1000; i++) { + final byte[] expected = new byte[i]; + random.nextBytes(expected); + final String hex = new Data(expected).hexEncodedString(); + final byte[] actual = Data.fromHexEncodedString(hex).value; + assertArrayEquals(expected, actual); + } + } + + @Test + public void testExtractMessages_iPhoneX_F() throws Exception { + final Data raw = Data.fromHexEncodedString("02011A020A0C0BFF4C001006071EA3DD89E014FF4C0001000000000000000000002000000000000000000000000000000000000000000000000000000000"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(2, messages.size()); + assertEquals("1006071EA3DD89E0", messages.get(0).hexEncodedString()); + assertEquals("0100000000000000000000200000000000", messages.get(1).hexEncodedString()); + + assertNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....1E"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^0100"}), raw)); + } + + @Test + public void testExtractMessages_iOS12() throws Exception { + final Data raw = Data.fromHexEncodedString("02011A14FF4C0001000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(1, messages.size()); + assertEquals("0100000000000000000000200000000000", messages.get(0).hexEncodedString()); + + assertNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^0100"}), raw)); + } + + @Test + public void testExtractMessages_iPhoneX_J() throws Exception { + final Data raw = Data.fromHexEncodedString("02011A020A0C0BFF4C0010060C1E4FDE4DF714FF4C0001000000000000000000002000000000000000000000000000000000000000000000000000000000"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(2, messages.size()); + assertEquals("10060C1E4FDE4DF7", messages.get(0).hexEncodedString()); + assertEquals("0100000000000000000000200000000000", messages.get(1).hexEncodedString()); + + assertNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....1E", "^10....14"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^0100"}), raw)); + } + + @Test + public void testExtractMessages_MacBookPro_F() throws Exception { + final Data raw = Data.fromHexEncodedString("0201060AFF4C001005421C1E616A000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(1, messages.size()); + assertEquals("1005421C1E616A", messages.get(0).hexEncodedString()); + + assertNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14"}), raw)); + assertNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^0100"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....1C", "^10....14"}), raw)); + } + + @Test + public void testExtractMessages_iPhoneSE1_with_Herald() throws Exception { + // iPhoneSE 1st gen w/ Herald + final Data raw = Data.fromHexEncodedString("02011a020a0c11079bfd5bd672451e80d3424647af328142"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertNull(messages); + } + + @Test + public void testExtractMessages_iPhoneSE1_Background() throws Exception { + // iPhoneSE 1st gen background + final Data raw = Data.fromHexEncodedString("02011a020a0c14ff4c000100000000000000000000200000000000"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(1, messages.size()); + assertEquals("0100000000000000000000200000000000", messages.get(0).hexEncodedString()); + + assertNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^0100"}), raw)); + } + + @Test + public void testExtractMessages_iPhoneX_A() throws Exception { + // iPhoneX + final Data raw = Data.fromHexEncodedString("1eff4c001219006d17255505df2aec6ef580be0ddeba8bb034c996de5b0200"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(1, messages.size()); + assertEquals("1219006d17255505df2aec6ef580be0ddeba8bb034c996de5b0200".toUpperCase(), messages.get(0).hexEncodedString()); + + assertNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14"}), raw)); + assertNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^0100"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14", "^12"}), raw)); + } + + @Test + public void testExtractMessages_iPhone7_A() throws Exception { + // iPhone7 + final Data raw = Data.fromHexEncodedString("0bff4c001006061a396363ce"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(1, messages.size()); + assertEquals("1006061a396363ce".toUpperCase(), messages.get(0).hexEncodedString()); + + assertNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14"}), raw)); + assertNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^0100"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14", "^10....1a"}), raw)); + } + + @Test + public void testExtractMessages_iPhone_nRFApp() throws Exception { + // nRFConnect app running on iPhone - a Valid device + final Data raw = Data.fromHexEncodedString("1bff4c000c0e00c857ac085510515d52cf3862211006551eee51497a"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(2, messages.size()); // AFC This returns 1 if parsed incorrectly + assertEquals("0c0e00c857ac085510515d52cf386221".toUpperCase(), messages.get(0).hexEncodedString()); // AFC too long - Current implementation incorrectly ignores the apple segment length, 0e + assertEquals("1006551eee51497a".toUpperCase(), messages.get(1).hexEncodedString()); // AFC This is not returned as a separate segment + + assertNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14"}), raw)); + assertNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^0100"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14", "^10....1E"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14", "^10....1e"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14", "^0C"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14", "^0c"}), raw)); + } + + @Test + public void testExtractMessages_AppleTV() throws Exception { + final Data raw = Data.fromHexEncodedString("02011a020a0c0aff4c00100508141bba69"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(1, messages.size()); + assertEquals("100508141BBA69", messages.get(0).hexEncodedString()); + + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14"}), raw)); + } + + @Test + public void testExtractMessages_Coincidence() throws Exception { + final Data raw = Data.fromHexEncodedString("02011a020a0c0aff4c0010050814ff4c00"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(1, messages.size()); + assertEquals("10050814FF4C00", messages.get(0).hexEncodedString()); + + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14"}), raw)); + } + + @Test + public void testExtractMessages_MultipleAppleSegments() throws Exception { + final Data raw = Data.fromHexEncodedString("02011a0dff4c0010050814123456100101"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(2, messages.size()); + assertEquals("10050814123456", messages.get(0).hexEncodedString()); + assertEquals("100101", messages.get(1).hexEncodedString()); + + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10..01"}), raw)); + } + + @Test + public void testExtractMessages_LegacyZeroOne() throws Exception { + final Data raw = Data.fromHexEncodedString("02011a020a0c0aff4c001005031c8ba89d14ff4c000100200000000000000000000000000000000000000000000000000000000000000000000000000000"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(2, messages.size()); + assertEquals("1005031c8ba89d".toUpperCase(), messages.get(0).hexEncodedString()); + assertEquals("0100200000000000000000000000000000", messages.get(1).hexEncodedString()); + + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....1C"}), raw)); + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^01[0-9A-F]{32}$"}), raw)); + } + + @Test + public void testExtractMessages_MacbookPro_A() throws Exception { + final Data raw = Data.fromHexEncodedString("02011a0aff4c001005031c0b4cac"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(1, messages.size()); + assertEquals("1005031C0B4CAC", messages.get(0).hexEncodedString()); + + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....1C"}), raw)); + } + + @Test + public void testExtractMessages_MacbookProUnderflow() throws Exception { + final Data raw = Data.fromHexEncodedString("02011a0aff4c001005031c0b4c"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertNull(messages); + } + + @Test + public void testExtractMessages_MacbookProOverflow() throws Exception { + final Data raw = Data.fromHexEncodedString("02011a0aff4c001005031c0b4cac02011a0aff4c00100503"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(1, messages.size()); + assertEquals("1005031C0B4CAC", messages.get(0).hexEncodedString()); + + assertNotNull(BLEDeviceFilter.match(BLEDeviceFilter.compilePatterns(new String[]{"^10....1C"}), raw)); + } + + + @Test + public void testCompilePatterns() throws Exception { + final List filterPatterns = BLEDeviceFilter.compilePatterns(new String[]{"^10....04", "^10....14"}); + assertEquals(2, filterPatterns.size()); + assertNotNull(BLEDeviceFilter.match(filterPatterns, "10060C044FDE4DF7")); + assertNotNull(BLEDeviceFilter.match(filterPatterns, "10060C144FDE4DF7")); + + // Ignoring dots + assertNotNull(BLEDeviceFilter.match(filterPatterns, "10XXXX044FDE4DF7")); + assertNotNull(BLEDeviceFilter.match(filterPatterns, "10XXXX144FDE4DF7")); + + // Not correct values + assertNull(BLEDeviceFilter.match(filterPatterns, "10060C054FDE4DF7")); + assertNull(BLEDeviceFilter.match(filterPatterns, "10060C154FDE4DF7")); + + // Not start of pattern + assertNull(BLEDeviceFilter.match(filterPatterns, "010060C054FDE4DF7")); + assertNull(BLEDeviceFilter.match(filterPatterns, "010060C154FDE4DF7")); + } + + @Test + public void testMatch_iPhoneX_F() throws Exception { + final Data raw = Data.fromHexEncodedString("02011A020A0C0BFF4C001006071EA3DD89E014FF4C0001000000000000000000002000000000000000000000000000000000000000000000000000000000"); + final List messages = BLEDeviceFilter.extractMessages(raw.value); + assertEquals(2, messages.size()); + assertEquals("1006071EA3DD89E0", messages.get(0).hexEncodedString()); + assertEquals("0100000000000000000000200000000000", messages.get(1).hexEncodedString()); + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/au/gov/health/covidsafe/sensor/datatype/PayloadDataTests.java b/app/src/androidTest/java/au/gov/health/covidsafe/sensor/datatype/PayloadDataTests.java new file mode 100644 index 0000000..99ad2a8 --- /dev/null +++ b/app/src/androidTest/java/au/gov/health/covidsafe/sensor/datatype/PayloadDataTests.java @@ -0,0 +1,21 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; + +public class PayloadDataTests { + + @Test + public void testShortName() throws Exception { + for (int i=0; i<600; i++) { + final PayloadData payloadData = new PayloadData((byte) 0, i); + assertNotNull(payloadData); + } + } + +} diff --git a/app/src/androidTest/java/au/gov/health/covidsafe/sensor/datatype/PseudoDeviceAddressTests.java b/app/src/androidTest/java/au/gov/health/covidsafe/sensor/datatype/PseudoDeviceAddressTests.java new file mode 100644 index 0000000..738d7e7 --- /dev/null +++ b/app/src/androidTest/java/au/gov/health/covidsafe/sensor/datatype/PseudoDeviceAddressTests.java @@ -0,0 +1,61 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 +// + +package au.gov.health.covidsafe.sensor.datatype; + +import org.junit.Test; + +import java.security.SecureRandom; +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +public class PseudoDeviceAddressTests { + + @Test + public void testSecureRandom() { + // Address should be different every time + long last = 0; + for (int i=0; i<1000; i++) { + final SecureRandom secureRandom = PseudoDeviceAddress.getSecureRandom(); + final long value = secureRandom.nextLong(); + assertNotEquals(last, value); + last = value; + } + } + + @Test + public void testEncodeDecode() { + // Test encoding and decoding to ensure same data means same address + for (int i=0; i<1000; i++) { + final PseudoDeviceAddress expected = new PseudoDeviceAddress(); + final PseudoDeviceAddress actual = new PseudoDeviceAddress(expected.data); + assertEquals(expected.address, actual.address); + } + } + + @Test + public void testRandomBytes() { + // Every byte should rotate (most of the time) + byte[] last = new byte[6]; + for (int i=0; i<10; i++) { + final PseudoDeviceAddress address = new PseudoDeviceAddress(); + assertEquals(6, address.data.length); + for (int j=0; j<6; j++) { + assertNotEquals(address.data[j], last[j]); + } + last = address.data; + } + } + + @Test + public void testVisualCheck() { + // Visual check for randomness and byte fill + for (int i=0; i<10; i++) { + final PseudoDeviceAddress address = new PseudoDeviceAddress(); + System.err.println(Arrays.toString(address.data)); + } + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ecd9195..2934ee6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,10 @@ + + + + () var appUpdateAvailableMessageResponseLiveData = MutableLiveData() @@ -136,4 +137,30 @@ class HomeActivity : FragmentActivity(), NetworkConnectionCheck.NetworkConnectio previousInternetConnection = isAvailable } + override fun sensor(sensor: SensorType?, didDetect: TargetIdentifier?) { + } + + override fun sensor(sensor: SensorType?, didRead: PayloadData?, fromTarget: TargetIdentifier?) { + } + + override fun sensor(sensor: SensorType?, didShare: MutableList?, fromTarget: TargetIdentifier?) { + } + + override fun sensor(sensor: SensorType?, didMeasure: Proximity?, fromTarget: TargetIdentifier?) { + } + + override fun sensor(sensor: SensorType?, didVisit: Location?) { + } + + override fun sensor(sensor: SensorType?, didMeasure: Proximity?, fromTarget: TargetIdentifier?, withPayload: PayloadData?, device: BLEDevice) { + } + override fun sensor(sensor: SensorType?, didMeasure: Proximity?, fromTarget: TargetIdentifier?, withPayload: PayloadData?) { + } + + override fun sensor(sensor: SensorType?, didUpdateState: SensorState?) { + } + + override fun sensor(sensor: SensorType?, didRead: PayloadData?, fromTarget: TargetIdentifier?, atProximity: Proximity?, withTxPower: Int, device: BLEDevice) { + } + } \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/app/TracerApp.kt b/app/src/main/java/au/gov/health/covidsafe/app/TracerApp.kt index 71f357e..214f2f2 100644 --- a/app/src/main/java/au/gov/health/covidsafe/app/TracerApp.kt +++ b/app/src/main/java/au/gov/health/covidsafe/app/TracerApp.kt @@ -4,40 +4,41 @@ import android.app.Application import android.content.Context import android.os.Build import au.gov.health.covidsafe.BuildConfig -import com.atlassian.mobilekit.module.feedback.FeedbackModule - import au.gov.health.covidsafe.logging.CentralLog import au.gov.health.covidsafe.services.BluetoothMonitoringService import au.gov.health.covidsafe.streetpass.CentralDevice import au.gov.health.covidsafe.streetpass.PeripheralDevice +import au.gov.health.covidsafe.ui.utils.Utils +import com.atlassian.mobilekit.module.feedback.FeedbackModule class TracerApp : Application() { override fun onCreate() { super.onCreate() - AppContext = applicationContext + AppContext = this.applicationContext FeedbackModule.init(this) - -// GetMessagesScheduler.scheduleGetMessagesJob() } companion object { - private const val TAG = "TracerApp" const val ORG = BuildConfig.ORG const val protocolVersion = BuildConfig.PROTOCOL_VERSION lateinit var AppContext: Context - fun thisDeviceMsg(): String { - BluetoothMonitoringService.broadcastMessage?.let { - CentralLog.i(TAG, "Retrieved BM for storage: $it") - return it + fun thisDeviceMsg(): String? { + val broadcastMessage = Utils.retrieveBroadcastMessage(AppContext) + if (broadcastMessage != null) { + return broadcastMessage + } else { + BluetoothMonitoringService.broadcastMessage?.let { + CentralLog.i(TAG, "Retrieved BM for storage: $it") + return it + } } - CentralLog.e(TAG, "No local Broadcast Message") - return BluetoothMonitoringService.broadcastMessage!! + return "" } fun asPeripheralDevice(): PeripheralDevice { @@ -47,6 +48,5 @@ class TracerApp : Application() { fun asCentralDevice(): CentralDevice { return CentralDevice(Build.MODEL, "SELF") } - } } diff --git a/app/src/main/java/au/gov/health/covidsafe/bluetooth/gatt/GattServer.kt b/app/src/main/java/au/gov/health/covidsafe/bluetooth/gatt/GattServer.kt index d963b49..e95ae23 100644 --- a/app/src/main/java/au/gov/health/covidsafe/bluetooth/gatt/GattServer.kt +++ b/app/src/main/java/au/gov/health/covidsafe/bluetooth/gatt/GattServer.kt @@ -78,7 +78,7 @@ class GattServer constructor(val context: Context, serviceUUIDString: String) { val readRequest = ReadRequestEncryptedPayload( System.currentTimeMillis() / 1000L, peripheral.modelP, - TracerApp.thisDeviceMsg() + TracerApp.thisDeviceMsg().toString() ) val plainRecord = gson.toJson(readRequest) diff --git a/app/src/main/java/au/gov/health/covidsafe/extensions/PermissionExtensions.kt b/app/src/main/java/au/gov/health/covidsafe/extensions/PermissionExtensions.kt index d537e0a..30c12e8 100644 --- a/app/src/main/java/au/gov/health/covidsafe/extensions/PermissionExtensions.kt +++ b/app/src/main/java/au/gov/health/covidsafe/extensions/PermissionExtensions.kt @@ -1,6 +1,6 @@ package au.gov.health.covidsafe.extensions -import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.Manifest.permission.* import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothManager import android.content.Context diff --git a/app/src/main/java/au/gov/health/covidsafe/notifications/NotificationTemplates.kt b/app/src/main/java/au/gov/health/covidsafe/notifications/NotificationTemplates.kt index 4e283f9..e3f18c9 100644 --- a/app/src/main/java/au/gov/health/covidsafe/notifications/NotificationTemplates.kt +++ b/app/src/main/java/au/gov/health/covidsafe/notifications/NotificationTemplates.kt @@ -4,12 +4,10 @@ import android.app.Notification import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.widget.RemoteViews import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import au.gov.health.covidsafe.HomeActivity import au.gov.health.covidsafe.R -import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.DAILY_UPLOAD_NOTIFICATION_CODE import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.PENDING_ACTIVITY import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.PENDING_WIZARD_REQ_CODE diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/DefaultSensorDelegate.java b/app/src/main/java/au/gov/health/covidsafe/sensor/DefaultSensorDelegate.java new file mode 100644 index 0000000..7b654b9 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/DefaultSensorDelegate.java @@ -0,0 +1,55 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor; + +import au.gov.health.covidsafe.sensor.ble.BLEDevice; +import au.gov.health.covidsafe.sensor.datatype.Location; +import au.gov.health.covidsafe.sensor.datatype.PayloadData; +import au.gov.health.covidsafe.sensor.datatype.Proximity; +import au.gov.health.covidsafe.sensor.datatype.SensorState; +import au.gov.health.covidsafe.sensor.datatype.SensorType; +import au.gov.health.covidsafe.sensor.datatype.TargetIdentifier; + +import java.util.List; + +/// Default implementation of SensorDelegate for making all interface methods optional. +public abstract class DefaultSensorDelegate implements SensorDelegate { + + @Override + public void sensor(SensorType sensor, TargetIdentifier didDetect) { + } + + @Override + public void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget) { + } + + @Override + public void sensor(SensorType sensor, List didShare, TargetIdentifier fromTarget) { + } + + @Override + public void sensor(SensorType sensor, Proximity didMeasure, TargetIdentifier fromTarget) { + } + + @Override + public void sensor(SensorType sensor, Location didVisit) { + } + + @Override + public void sensor(SensorType sensor, Proximity didMeasure, TargetIdentifier fromTarget, PayloadData withPayload, BLEDevice device) { + } + + @Override + public void sensor(SensorType sensor, Proximity didMeasure, TargetIdentifier fromTarget, PayloadData withPayload) { + } + + @Override + public void sensor(SensorType sensor, SensorState didUpdateState) { + } + + @Override + public void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget, Proximity atProximity, int withTxPower, BLEDevice device) { + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/Sensor.java b/app/src/main/java/au/gov/health/covidsafe/sensor/Sensor.java new file mode 100644 index 0000000..6fe8483 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/Sensor.java @@ -0,0 +1,17 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor; + +/// Sensor for detecting and tracking various kinds of disease transmission vectors, e.g. contact with people, time at location. +public interface Sensor { + /// Add delegate for responding to sensor events. + void add(SensorDelegate delegate); + + /// Start sensing. + void start(); + + /// Stop sensing. + void stop(); +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/SensorArray.java b/app/src/main/java/au/gov/health/covidsafe/sensor/SensorArray.java new file mode 100644 index 0000000..447bff7 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/SensorArray.java @@ -0,0 +1,93 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +import au.gov.health.covidsafe.BuildConfig; +import au.gov.health.covidsafe.sensor.ble.ConcreteBLESensor; +import au.gov.health.covidsafe.sensor.data.BatteryLog; +import au.gov.health.covidsafe.sensor.data.ConcreteSensorLogger; +import au.gov.health.covidsafe.sensor.data.ContactLog; +import au.gov.health.covidsafe.sensor.data.DetectionLog; +import au.gov.health.covidsafe.sensor.data.SensorLogger; +import au.gov.health.covidsafe.sensor.data.StatisticsLog; +import au.gov.health.covidsafe.sensor.datatype.PayloadData; +import au.gov.health.covidsafe.sensor.datatype.PayloadTimestamp; +import au.gov.health.covidsafe.sensor.payload.PayloadDataSupplier; + +import java.util.ArrayList; +import java.util.List; + +/// Sensor array for combining multiple detection and tracking methods. +public class SensorArray implements Sensor { + private final Context context; + private final SensorLogger logger = new ConcreteSensorLogger("Sensor", "SensorArray"); + private final List sensorArray = new ArrayList<>(); + + private final PayloadData payloadData; + public final static String deviceDescription = android.os.Build.MODEL + " (Android " + android.os.Build.VERSION.SDK_INT + ")"; + + + public SensorArray(Context context, PayloadDataSupplier payloadDataSupplier) { + this.context = context; + // Ensure logger has been initialised (should have happened in AppDelegate already) + ConcreteSensorLogger.context(context); + logger.debug("init"); + + // Start foreground service to enable background scan +// final Intent intent = new Intent(context, ForegroundService.class); + //final Intent intentBleService = new Intent(context, BluetoothMonitoringService.class); + //context.startService(intentBleService); +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +// context.startForegroundService(intent); +// } else { +// context.startService(intent); +// } + + // Define sensor array + sensorArray.add(new ConcreteBLESensor(context, payloadDataSupplier)); + + // Loggers + payloadData = payloadDataSupplier.payload(new PayloadTimestamp()); + if (BuildConfig.DEBUG) { + add(new ContactLog(context, "contacts.csv")); + add(new StatisticsLog(context, "statistics.csv", payloadData)); + add(new DetectionLog(context,"detection.csv", payloadData)); + new BatteryLog(context, "battery.csv"); + } + //Get Device payload + logger.info("DEVICE (payload={},description={})", payloadData.shortName(), deviceDescription); + } + + public final PayloadData payloadData() { + return payloadData; + } + + @Override + public void add(final SensorDelegate delegate) { + for (Sensor sensor : sensorArray) { + sensor.add(delegate); + } + } + + @Override + public void start() { + logger.debug("start"); + for (Sensor sensor : sensorArray) { + sensor.start(); + } + } + + @Override + public void stop() { + logger.debug("stop"); + for (Sensor sensor : sensorArray) { + sensor.stop(); + } + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/SensorDelegate.java b/app/src/main/java/au/gov/health/covidsafe/sensor/SensorDelegate.java new file mode 100644 index 0000000..b9136ab --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/SensorDelegate.java @@ -0,0 +1,44 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor; + +import au.gov.health.covidsafe.sensor.ble.BLEDevice; +import au.gov.health.covidsafe.sensor.datatype.Location; +import au.gov.health.covidsafe.sensor.datatype.PayloadData; +import au.gov.health.covidsafe.sensor.datatype.Proximity; +import au.gov.health.covidsafe.sensor.datatype.SensorState; +import au.gov.health.covidsafe.sensor.datatype.SensorType; +import au.gov.health.covidsafe.sensor.datatype.TargetIdentifier; + +import java.util.List; + +/// Sensor delegate for receiving sensor events. +public interface SensorDelegate { + /// Detection of a target with an ephemeral identifier, e.g. BLE central detecting a BLE peripheral. + void sensor(SensorType sensor, TargetIdentifier didDetect); + + /// Read payload data from target, e.g. encrypted device identifier from BLE peripheral after successful connection. + void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget); + + /// Read payload data of other targets recently acquired by a target, e.g. Android peripheral sharing payload data acquired from nearby iOS peripherals. + void sensor(SensorType sensor, List didShare, TargetIdentifier fromTarget); + + /// Measure proximity to target, e.g. a sample of RSSI values from BLE peripheral. + void sensor(SensorType sensor, Proximity didMeasure, TargetIdentifier fromTarget); + + /// Detection of time spent at location, e.g. at specific restaurant between 02/06/2020 19:00 and 02/06/2020 21:00 + void sensor(SensorType sensor, Location didVisit); + + /// Measure proximity to target with payload data. Combines didMeasure and didRead into a single convenient delegate method + void sensor(SensorType sensor, Proximity didMeasure, TargetIdentifier fromTarget, PayloadData withPayload, BLEDevice device); + + void sensor(SensorType sensor, Proximity didMeasure, TargetIdentifier fromTarget, PayloadData withPayload); + + /// Sensor state update + void sensor(SensorType sensor, SensorState didUpdateState); + + /// Measure proximity to target with payload data. Combines didMeasure and didRead into a single convenient delegate method + void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget, Proximity atProximity, int withTxPower, BLEDevice device); +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDatabase.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDatabase.java new file mode 100644 index 0000000..f61ede2 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDatabase.java @@ -0,0 +1,38 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.ScanResult; + +import au.gov.health.covidsafe.sensor.datatype.PayloadData; +import au.gov.health.covidsafe.sensor.datatype.PayloadSharingData; +import au.gov.health.covidsafe.sensor.datatype.TargetIdentifier; + +import java.util.List; + +/// Registry for collating fragments of information from asynchronous BLE operations. +public interface BLEDatabase { + /// Add delegate for handling database events + void add(BLEDatabaseDelegate delegate); + + /// Get or create device for collating information from asynchronous BLE operations. + BLEDevice device(ScanResult scanResult); + + /// Get or create device for collating information from asynchronous BLE operations. + BLEDevice device(BluetoothDevice bluetoothDevice); + + /// Get or create device for collating information from asynchronous BLE operations. + BLEDevice device(PayloadData payloadData); + + /// Get all devices + List devices(); + + /// Delete + void delete(TargetIdentifier identifier); + + /// Get payload sharing data for a peer + PayloadSharingData payloadSharingData(BLEDevice peer); +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDatabaseDelegate.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDatabaseDelegate.java new file mode 100644 index 0000000..39f45e8 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDatabaseDelegate.java @@ -0,0 +1,14 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +/// Delegate for receiving registry create/update/delete events +public interface BLEDatabaseDelegate { + void bleDatabaseDidCreate(BLEDevice device); + + void bleDatabaseDidUpdate(BLEDevice device, BLEDeviceAttribute attribute); + + void bleDatabaseDidDelete(BLEDevice device); +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDevice.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDevice.java new file mode 100644 index 0000000..cb42e96 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDevice.java @@ -0,0 +1,330 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.le.ScanRecord; +import au.gov.health.covidsafe.sensor.datatype.PayloadData; +import au.gov.health.covidsafe.sensor.datatype.PseudoDeviceAddress; +import au.gov.health.covidsafe.sensor.datatype.RSSI; +import au.gov.health.covidsafe.sensor.datatype.TargetIdentifier; +import au.gov.health.covidsafe.sensor.datatype.TimeInterval; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Queue; + +public class BLEDevice { + /// Device registration timestamp + public final Date createdAt; + /// Last time anything changed, e.g. attribute update + public Date lastUpdatedAt; + /// Ephemeral device identifier, e.g. peripheral identifier UUID + public final TargetIdentifier identifier; + /// Pseudo device address for tracking Android devices that change address constantly. + private PseudoDeviceAddress pseudoDeviceAddress; + /// Delegate for listening to attribute updates events. + private final BLEDeviceDelegate delegate; + /// Android Bluetooth device object for interacting with this device. + private BluetoothDevice peripheral; + /// Bluetooth device connection state. + private BLEDeviceState state = BLEDeviceState.disconnected; + /// Device operating system, this is necessary for selecting different interaction procedures for each platform. + private BLEDeviceOperatingSystem operatingSystem = BLEDeviceOperatingSystem.unknown; + /// Payload data acquired from the device via payloadCharacteristic read, e.g. C19X beacon code or Sonar encrypted identifier + private PayloadData payloadData; + /// Most recent RSSI measurement taken by readRSSI or didDiscover. + private RSSI rssi; + /// Transmit power data where available (only provided by Android devices) + private BLE_TxPower txPower; + /// Is device receive only? + private boolean receiveOnly = false; + /// Ignore logic + private TimeInterval ignoreForDuration = null; + private Date ignoreUntil = null; + private ScanRecord scanRecord = null; + /// BLE characteristics + private BluetoothGattCharacteristic signalCharacteristic = null; + private BluetoothGattCharacteristic payloadCharacteristic = null; + private BluetoothGattCharacteristic legacyPayloadCharacteristic = null; + protected byte[] signalCharacteristicWriteValue = null; + protected Queue signalCharacteristicWriteQueue = null; + + /// Track connection timestamps + private Date lastDiscoveredAt = null; + private Date lastConnectedAt = null; + + /// Payload data already shared with this peer + protected final List payloadSharingData = new ArrayList<>(); + + /// Track write timestamps + private Date lastWritePayloadAt = null; + private Date lastWriteRssiAt = null; + private Date lastWritePayloadSharingAt = null; + private Date payloadDataLastUpdatedAt = null; + + public TimeInterval timeIntervalSinceConnected() { + if (state() != BLEDeviceState.connected) { + return TimeInterval.zero; + } + if (lastConnectedAt == null) { + return TimeInterval.zero; + } + return new TimeInterval((new Date().getTime() - lastConnectedAt.getTime()) / 1000); + } + + /// Time interval since last attribute value update, this is used to identify devices that may have expired and should be removed from the database. + public TimeInterval timeIntervalSinceLastUpdate() { + return new TimeInterval((new Date().getTime() - lastUpdatedAt.getTime()) / 1000); + } + + public String description() { + return "BLEDevice[id=" + identifier + ",os=" + operatingSystem + ",payload=" + payloadData() + ",address=" + pseudoDeviceAddress() + "]"; + } + + public BLEDevice(TargetIdentifier identifier, BLEDeviceDelegate delegate) { + this.createdAt = new Date(); + this.identifier = identifier; + this.delegate = delegate; + this.lastUpdatedAt = createdAt; + } + + /// Create a clone of an existing device + public BLEDevice(BLEDevice device, BluetoothDevice bluetoothDevice) { + this.createdAt = device.createdAt; + this.lastUpdatedAt = new Date(); + this.identifier = new TargetIdentifier(bluetoothDevice); + this.pseudoDeviceAddress = device.pseudoDeviceAddress; + this.delegate = device.delegate; + this.state = device.state; + this.operatingSystem = device.operatingSystem; + this.payloadData = device.payloadData; + this.rssi = device.rssi; + this.txPower = device.txPower; + this.receiveOnly = device.receiveOnly; + this.ignoreForDuration = device.ignoreForDuration; + this.ignoreUntil = device.ignoreUntil; + this.scanRecord = device.scanRecord; + this.signalCharacteristic = device.signalCharacteristic; + this.payloadCharacteristic = device.payloadCharacteristic; + this.signalCharacteristicWriteValue = device.signalCharacteristicWriteValue; + this.signalCharacteristicWriteQueue = device.signalCharacteristicWriteQueue; + this.legacyPayloadCharacteristic = device.legacyPayloadCharacteristic; + this.lastDiscoveredAt = device.lastDiscoveredAt; + this.lastConnectedAt = device.lastConnectedAt; + this.payloadSharingData.addAll(device.payloadSharingData); + this.lastWritePayloadAt = device.lastWritePayloadAt; + this.lastWriteRssiAt = device.lastWriteRssiAt; + this.lastWritePayloadSharingAt = device.lastWritePayloadSharingAt; + this.payloadDataLastUpdatedAt = device.payloadDataLastUpdatedAt; + } + + public PseudoDeviceAddress pseudoDeviceAddress() { + return pseudoDeviceAddress; + } + + public void pseudoDeviceAddress(PseudoDeviceAddress pseudoDeviceAddress) { + if (this.pseudoDeviceAddress == null || !this.pseudoDeviceAddress.equals(pseudoDeviceAddress)) { + this.pseudoDeviceAddress = pseudoDeviceAddress; + lastUpdatedAt = new Date(); + } + } + + public BluetoothDevice peripheral() { + return peripheral; + } + + public void peripheral(BluetoothDevice peripheral) { + if (this.peripheral != peripheral) { + this.peripheral = peripheral; + lastUpdatedAt = new Date(); + } + } + + public BLEDeviceState state() { + return state; + } + + public void state(BLEDeviceState state) { + this.state = state; + lastUpdatedAt = new Date(); + if (state == BLEDeviceState.connected) { + lastConnectedAt = lastUpdatedAt; + } + delegate.device(this, BLEDeviceAttribute.state); + } + + public BLEDeviceOperatingSystem operatingSystem() { + return operatingSystem; + } + + public void operatingSystem(BLEDeviceOperatingSystem operatingSystem) { + lastUpdatedAt = new Date(); + // Set ignore timer + if (operatingSystem == BLEDeviceOperatingSystem.ignore) { + if (ignoreForDuration == null) { + ignoreForDuration = TimeInterval.minute; + } else if (ignoreForDuration.value < TimeInterval.minutes(3).value) { + ignoreForDuration = new TimeInterval(Math.round(ignoreForDuration.value * 1.2)); + } + ignoreUntil = new Date(lastUpdatedAt.getTime() + ignoreForDuration.millis()); + } else { + ignoreForDuration = null; + ignoreUntil = null; + } + if (this.operatingSystem != operatingSystem) { + this.operatingSystem = operatingSystem; + delegate.device(this, BLEDeviceAttribute.operatingSystem); + } + } + + /// Should ignore this device for now. + public boolean ignore() { + if (ignoreUntil == null) { + return false; + } + if (new Date().getTime() < ignoreUntil.getTime()) { + return true; + } + return false; + } + + public PayloadData payloadData() { + return payloadData; + } + + public void payloadData(PayloadData payloadData) { + this.payloadData = payloadData; + lastUpdatedAt = new Date(); + payloadDataLastUpdatedAt = lastUpdatedAt; + delegate.device(this, BLEDeviceAttribute.payloadData); + } + + public RSSI rssi() { + return rssi; + } + + //Please check the places that call it and save data + public void rssi(RSSI rssi) { + this.rssi = rssi; + lastUpdatedAt = new Date(); + delegate.device(this, BLEDeviceAttribute.rssi); + } + + public void legacyPayloadCharacteristic(BluetoothGattCharacteristic characteristic) { + this.legacyPayloadCharacteristic = characteristic; + lastUpdatedAt = new Date(); + } + + public BluetoothGattCharacteristic getLegacyPayloadCharacteristic() { + return legacyPayloadCharacteristic; + } + + public BLE_TxPower txPower() { + return txPower; + } + + public void txPower(BLE_TxPower txPower) { + this.txPower = txPower; + lastUpdatedAt = new Date(); + delegate.device(this, BLEDeviceAttribute.txPower); + } + + public boolean receiveOnly() { + return receiveOnly; + } + + public void receiveOnly(boolean receiveOnly) { + this.receiveOnly = receiveOnly; + lastUpdatedAt = new Date(); + } + + public void invalidateCharacteristics() { + signalCharacteristic = null; + payloadCharacteristic = null; + legacyPayloadCharacteristic = null; + } + + public BluetoothGattCharacteristic signalCharacteristic() { + return signalCharacteristic; + } + + public void signalCharacteristic(BluetoothGattCharacteristic characteristic) { + this.signalCharacteristic = characteristic; + lastUpdatedAt = new Date(); + } + + public BluetoothGattCharacteristic payloadCharacteristic() { + return payloadCharacteristic; + } + + public void payloadCharacteristic(BluetoothGattCharacteristic characteristic) { + this.payloadCharacteristic = characteristic; + lastUpdatedAt = new Date(); + } + + public void registerDiscovery() { + lastDiscoveredAt = new Date(); + lastUpdatedAt = lastDiscoveredAt; + } + + public void registerWritePayload() { + lastUpdatedAt = new Date(); + lastWritePayloadAt = lastUpdatedAt; + } + + public TimeInterval timeIntervalSinceLastWritePayload() { + if (lastWritePayloadAt == null) { + return TimeInterval.never; + } + return new TimeInterval((new Date().getTime() - lastWritePayloadAt.getTime()) / 1000); + } + + public void registerWriteRssi() { + lastUpdatedAt = new Date(); + lastWriteRssiAt = lastUpdatedAt; + } + + public TimeInterval timeIntervalSinceLastWriteRssi() { + if (lastWriteRssiAt == null) { + return TimeInterval.never; + } + return new TimeInterval((new Date().getTime() - lastWriteRssiAt.getTime()) / 1000); + } + + public TimeInterval timeIntervalSinceLastPayloadUpdate() { + if (payloadDataLastUpdatedAt == null) { + return TimeInterval.never; + } + return new TimeInterval((new Date().getTime() - payloadDataLastUpdatedAt.getTime()) / 1000); + } + + public void registerWritePayloadSharing() { + lastUpdatedAt = new Date(); + lastWritePayloadSharingAt = lastUpdatedAt; + } + + public TimeInterval timeIntervalSinceLastWritePayloadSharing() { + if (lastWritePayloadSharingAt == null) { + return TimeInterval.never; + } + return new TimeInterval((new Date().getTime() - lastWritePayloadSharingAt.getTime()) / 1000); + } + + public void scanRecord(ScanRecord scanRecord) { + this.scanRecord = scanRecord; + } + + public ScanRecord scanRecord() { + return scanRecord; + } + + @Override + public String toString() { + return description(); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceAttribute.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceAttribute.java new file mode 100644 index 0000000..7492971 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceAttribute.java @@ -0,0 +1,9 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +public enum BLEDeviceAttribute { + peripheral, state, operatingSystem, payloadData, rssi, txPower +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceDelegate.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceDelegate.java new file mode 100644 index 0000000..f1a15c1 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceDelegate.java @@ -0,0 +1,10 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +public interface BLEDeviceDelegate { + void device(BLEDevice device, BLEDeviceAttribute didUpdate); +} + diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceOperatingSystem.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceOperatingSystem.java new file mode 100644 index 0000000..cb94837 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceOperatingSystem.java @@ -0,0 +1,9 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +public enum BLEDeviceOperatingSystem { + android_tbc, android, ios_tbc, ios, ignore, shared, unknown +} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceState.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceState.java new file mode 100644 index 0000000..b9f9d12 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDeviceState.java @@ -0,0 +1,10 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +/// BLE device connection state +public enum BLEDeviceState { + connecting, connected, disconnected +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEReceiver.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEReceiver.java new file mode 100644 index 0000000..0b2cd86 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEReceiver.java @@ -0,0 +1,18 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +import au.gov.health.covidsafe.sensor.Sensor; +import au.gov.health.covidsafe.sensor.SensorDelegate; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Beacon receiver scans for peripherals with fixed service UUID. + */ +public interface BLEReceiver extends Sensor { + Queue delegates = new ConcurrentLinkedQueue<>(); +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLESensor.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLESensor.java new file mode 100644 index 0000000..d3c2eda --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLESensor.java @@ -0,0 +1,10 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +import au.gov.health.covidsafe.sensor.Sensor; + +public interface BLESensor extends Sensor { +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLESensorConfiguration.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLESensorConfiguration.java new file mode 100644 index 0000000..7c2f3ce --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLESensorConfiguration.java @@ -0,0 +1,64 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +import au.gov.health.covidsafe.BuildConfig; +import au.gov.health.covidsafe.sensor.data.SensorLoggerLevel; +import au.gov.health.covidsafe.sensor.datatype.TimeInterval; + +import java.util.UUID; + +/// Defines BLE sensor configuration data, e.g. service and characteristic UUIDs +public class BLESensorConfiguration { + public final static SensorLoggerLevel logLevel = SensorLoggerLevel.debug; + /** + * Service UUID for beacon service. This is a fixed UUID to enable iOS devices to find each other even + * in background mode. Android devices will need to find Apple devices first using the manufacturer code + * then discover services to identify actual beacons. + */ + public final static UUID serviceUUID = UUID.fromString(BuildConfig.BLE_SSID); + /// Signaling characteristic for controlling connection between peripheral and central, e.g. keep each other from suspend state + public final static UUID androidSignalCharacteristicUUID = UUID.fromString(BuildConfig.BLE_ANDROIDSIGNALCHARACTERISTIC); + /// Signaling characteristic for controlling connection between peripheral and central, e.g. keep each other from suspend state + public final static UUID iosSignalCharacteristicUUID = UUID.fromString(BuildConfig.BLE_IOSSIGNALCHARACTERISTIC); + /// Primary payload characteristic (read) for distributing payload data from peripheral to central, e.g. identity data + public final static UUID payloadCharacteristicUUID = UUID.fromString(BuildConfig.BLE_PAYLOADCHARACTERISTIC); + public final static UUID legacyCovidsafePayloadCharacteristicUUID = UUID.fromString(BuildConfig.BLE_SSID); + /// Expiry time for shared payloads, to ensure only recently seen payloads are shared, Sharing disabled for now as location permisssion on ios will allow scanning to work + public static TimeInterval payloadSharingExpiryTimeInterval = TimeInterval.zero; + /// Manufacturer data is being used on Android to store pseudo device address + public final static int manufacturerIdForSensor = 65530; + /// Advert refresh time interval + public final static TimeInterval advertRefreshTimeInterval = TimeInterval.minutes(15); + + /// Signal characteristic action code for write payload, expect 1 byte action code followed by 2 byte little-endian Int16 integer value for payload data length, then payload data + public final static byte signalCharacteristicActionWritePayload = (byte) 1; + /// Signal characteristic action code for write RSSI, expect 1 byte action code followed by 4 byte little-endian Int32 integer value for RSSI value + public final static byte signalCharacteristicActionWriteRSSI = (byte) 2; + /// Signal characteristic action code for write payload, expect 1 byte action code followed by 2 byte little-endian Int16 integer value for payload sharing data length, then payload sharing data + public final static byte signalCharacteristicActionWritePayloadSharing = (byte) 3; + + // BLE advert manufacturer ID for Apple, for scanning of background iOS devices + public final static int manufacturerIdForApple = 76; + + /// Filter duplicate payload data and suppress sensor(didRead:fromTarget) delegate calls + public static TimeInterval filterDuplicatePayloadData = TimeInterval.minutes(30); + + /// Define device filtering rules based on message patterns + /// - Avoids connections to devices that cannot host sensor services + /// - Matches against every manufacturer specific data message (Hex format) in advert + /// - Java regular expression patterns, case insensitive, find pattern anywhere in message + /// - Remember to include ^ to match from start of message + /// - Use deviceFilterTrainingEnabled in development environment to identify patterns + public static String[] deviceFilterFeaturePatterns = new String[]{ + "^10....04", + "^10....14", + "^0100000000000000000000000000000000", + "^05","^07","^09", + "^00", + "^08","^03","^06", + "^0C","^0D","^0F","^0E","^0B" + }; +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLETimer.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLETimer.java new file mode 100644 index 0000000..0a2e42f --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLETimer.java @@ -0,0 +1,123 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +import android.content.Context; +import android.os.PowerManager; + +import au.gov.health.covidsafe.sensor.data.ConcreteSensorLogger; +import au.gov.health.covidsafe.sensor.data.SensorLogger; +import au.gov.health.covidsafe.sensor.datatype.Sample; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Steady one second timer for controlling BLE operations. Having a reliable timer for starting + * and stopping scans is fundamental for reliable detection and tracking. Methods that have been + * tested and failed included : + * 1. Handler.postDelayed loop backed by MainLooper + * - Actual delay time can drift to 10+ minutes for a 4 second request. + * 2. Handler.postDelayed loop backed by dedicated looper backed by dedicated HandlerThread + * - Slightly better than option (1) but actual delay time can still drift to several minutes for a 4 second request. + * 3. Timer scheduled task loop + * - Timer can drift + *

+ * Test impact of power management by ... + *

+ * 1. Run app on device + * 2. Keep one device connected via USB only + * 3. Put app in background mode and lock device + * 4. Go to terminal + * 5. cd ~/Library/Android/sdk/platform-tools + *

+ * Test DOZE mode + * 1. ./adb shell dumpsys battery unplug + * 2. Expect "powerSource=battery" on log + * 3. ./adb shell dumpsys deviceidle force-idle + * 4. Expect "idle=true" on log + *

+ * Exit DOZE mode + * 1. ./adb shell dumpsys deviceidle unforce + * 2. ./adb shell dumpsys battery reset + * 3. Expect "idle=false" and "powerSource=usb/ac" on log + *

+ * Test APP STANDBY mode + * 1. ./adb shell dumpsys battery unplug + * 2. Expect "powerSource=battery" + * 3. ./adb shell am set-inactive au.gov.health.covidsafe true + *

+ * Exit APP STANDBY mode + * 1. ./adb shell am set-inactive au.gov.health.covidsafe false + * 2. ./adb shell am get-inactive au.gov.health.covidsafe + * 3. Expect "idle=false" on terminal + * 4. ./adb shell dumpsys battery reset + * 5. Expect "powerSource=usb/ac" on log + */ +public class BLETimer { + private final SensorLogger logger = new ConcreteSensorLogger("Sensor", "BLETimer"); + private final Sample sample = new Sample(); + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final PowerManager.WakeLock wakeLock; + private final AtomicLong now = new AtomicLong(0); + private final Queue delegates = new ConcurrentLinkedQueue<>(); + private final Runnable runnable = new Runnable() { + @Override + public void run() { + for (BLETimerDelegate delegate : delegates) { + try { + delegate.bleTimer(now.get()); + } catch (Throwable e) { + logger.fault("delegate execution failed", e); + } + } + } + }; + + public BLETimer(Context context) { + final PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Sensor:BLETimer"); + wakeLock.acquire(); + final Thread timerThread = new Thread(new Runnable() { + private long last = 0; + + @Override + public void run() { + while (true) { + now.set(System.currentTimeMillis()); + final long elapsed = now.get() - last; + if (elapsed >= 1000) { + if (last != 0) { + sample.add(elapsed); + executorService.execute(runnable); + } + last = now.get(); + } + try { + Thread.sleep(500); + } catch (Throwable e) { + logger.fault("Timer interrupted", e); + } + } + } + }); + timerThread.setPriority(Thread.MAX_PRIORITY); + timerThread.setName("Sensor.BLETimer"); + timerThread.start(); + } + + @Override + protected void finalize() { + wakeLock.release(); + } + + /// Add delegate for time notification + public void add(BLETimerDelegate delegate) { + delegates.add(delegate); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLETimerDelegate.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLETimerDelegate.java new file mode 100644 index 0000000..3631b03 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLETimerDelegate.java @@ -0,0 +1,10 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +public interface BLETimerDelegate { + + void bleTimer(long currentTimeMillis); +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLETransmitter.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLETransmitter.java new file mode 100644 index 0000000..dc1e120 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLETransmitter.java @@ -0,0 +1,39 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +import au.gov.health.covidsafe.sensor.Sensor; +import au.gov.health.covidsafe.sensor.SensorDelegate; +import au.gov.health.covidsafe.sensor.datatype.PayloadData; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Beacon transmitter broadcasts a fixed service UUID to enable background scan by iOS. When iOS + * enters background mode, the UUID will disappear from the broadcast, so Android devices need to + * search for Apple devices and then connect and discover services to read the UUID. + */ +public interface BLETransmitter extends Sensor { + /** + * Delegates for receiving beacon detection events. This is necessary because some Android devices (Samsung J6) + * does not support BLE transmit, thus making the beacon characteristic writable offers a mechanism for such devices + * to detect a beacon transmitter and make their own presence known by sending its own beacon code and RSSI as + * data to the transmitter. + */ + Queue delegates = new ConcurrentLinkedQueue<>(); + + /** + * Get current payload. + */ + PayloadData payloadData(); + + /** + * Is transmitter supported. + * + * @return True if BLE advertising is supported. + */ + boolean isSupported(); +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLE_TxPower.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLE_TxPower.java new file mode 100644 index 0000000..b652f50 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLE_TxPower.java @@ -0,0 +1,13 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +public class BLE_TxPower { + public final int value; + + public BLE_TxPower(int value) { + this.value = value; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BluetoothStateManager.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BluetoothStateManager.java new file mode 100644 index 0000000..27c4388 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BluetoothStateManager.java @@ -0,0 +1,16 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +import au.gov.health.covidsafe.sensor.datatype.BluetoothState; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +public interface BluetoothStateManager { + Queue delegates = new ConcurrentLinkedQueue<>(); + + BluetoothState state(); +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BluetoothStateManagerDelegate.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BluetoothStateManagerDelegate.java new file mode 100644 index 0000000..be92db0 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BluetoothStateManagerDelegate.java @@ -0,0 +1,11 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +import au.gov.health.covidsafe.sensor.datatype.BluetoothState; + +public interface BluetoothStateManagerDelegate { + void bluetoothStateManager(BluetoothState didUpdateState); +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLEDatabase.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLEDatabase.java new file mode 100644 index 0000000..21803e6 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLEDatabase.java @@ -0,0 +1,350 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.ScanRecord; +import android.bluetooth.le.ScanResult; +import android.util.Log; + +import au.gov.health.covidsafe.sensor.data.ConcreteSensorLogger; +import au.gov.health.covidsafe.sensor.data.SensorLogger; +import au.gov.health.covidsafe.sensor.datatype.Data; +import au.gov.health.covidsafe.sensor.datatype.PayloadData; +import au.gov.health.covidsafe.sensor.datatype.PayloadSharingData; +import au.gov.health.covidsafe.sensor.datatype.PseudoDeviceAddress; +import au.gov.health.covidsafe.sensor.datatype.RSSI; +import au.gov.health.covidsafe.sensor.datatype.TargetIdentifier; +import au.gov.health.covidsafe.streetpass.persistence.Encryption; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ConcreteBLEDatabase implements BLEDatabase, BLEDeviceDelegate { + private final SensorLogger logger = new ConcreteSensorLogger("Sensor", "BLE.ConcreteBLEDatabase"); + private final Queue delegates = new ConcurrentLinkedQueue<>(); + private final Map database = new ConcurrentHashMap<>(); + private final ExecutorService queue = Executors.newSingleThreadExecutor(); + private static final String LOG_TAG = ConcreteBLEDatabase.class.getSimpleName(); + + @Override + public void add(BLEDatabaseDelegate delegate) { + delegates.add(delegate); + } + + @Override + public BLEDevice device(ScanResult scanResult) { + final BluetoothDevice bluetoothDevice = scanResult.getDevice(); + // Get pseudo device address + final PseudoDeviceAddress pseudoDeviceAddress = pseudoDeviceAddress(scanResult); + if (pseudoDeviceAddress == null) { + // Get device based on peripheral only + return device(bluetoothDevice); + } + // Identify all existing devices with the same pseudo device address + final List candidates = new ArrayList<>(); + for (final BLEDevice device : database.values()) { + if (device.pseudoDeviceAddress() == null) { + continue; + } + if (device.pseudoDeviceAddress().equals(pseudoDeviceAddress)) { + candidates.add(device); + } + } + // No existing device matching pseudo device address, create new device + if (candidates.size() == 0) { + final BLEDevice device = device(bluetoothDevice); + device.pseudoDeviceAddress(pseudoDeviceAddress); + return device; + } + // Find device with the same target identifier + final TargetIdentifier targetIdentifier = new TargetIdentifier(bluetoothDevice); + final BLEDevice existingDevice = database.get(targetIdentifier); + if (existingDevice != null) { + existingDevice.pseudoDeviceAddress(pseudoDeviceAddress); + shareDataAcrossDevices(pseudoDeviceAddress); + return existingDevice; + } + // Get most recent version of the device and clone to enable attachment to new peripheral + Collections.sort(candidates, new Comparator() { + @Override + public int compare(BLEDevice d0, BLEDevice d1) { + return Long.compare(d1.lastUpdatedAt.getTime(), d0.lastUpdatedAt.getTime()); + } + }); + final BLEDevice cloneSource = candidates.get(0); + final BLEDevice newDevice = new BLEDevice(cloneSource, scanResult.getDevice()); + database.put(newDevice.identifier, newDevice); + queue.execute(new Runnable() { + @Override + public void run() { + logger.debug("create (device={},pseudoAddress={})", newDevice.identifier, pseudoDeviceAddress); + for (BLEDatabaseDelegate delegate : delegates) { + delegate.bleDatabaseDidCreate(newDevice); + } + } + }); + newDevice.peripheral(scanResult.getDevice()); + final PayloadData payloadData = shareDataAcrossDevices(pseudoDeviceAddress); + if (payloadData != null) { + newDevice.payloadData(payloadData); + } + return newDevice; + } + + /// Get pseudo device address for Android devices + private PseudoDeviceAddress pseudoDeviceAddress(final ScanResult scanResult) { + Log.d(LOG_TAG, "PseudoDeviceAddress"); + final ScanRecord scanRecord = scanResult.getScanRecord(); + if (scanRecord == null) { + return null; + } + final byte[] data = scanRecord.getManufacturerSpecificData(BLESensorConfiguration.manufacturerIdForSensor); +// if (data == null || data.length != 6) { +// return null; +// } + if (data == null) { + return null; + } + return new PseudoDeviceAddress(data); + } + + /// Share information across devices with the same pseudo device address + private PayloadData shareDataAcrossDevices(final PseudoDeviceAddress pseudoDeviceAddress) { + // Get all devices with the same pseudo device address + final List devices = new ArrayList<>(); + for (final BLEDevice device : database.values()) { + if (device.pseudoDeviceAddress() == null) { + continue; + } + if (device.pseudoDeviceAddress().equals(pseudoDeviceAddress)) { + devices.add(device); + } + } + // Get most recent version of payload data + Collections.sort(devices, new Comparator() { + @Override + public int compare(BLEDevice d0, BLEDevice d1) { + return Long.compare(d1.lastUpdatedAt.getTime(), d0.lastUpdatedAt.getTime()); + } + }); + PayloadData payloadData = null; + for (BLEDevice device : devices) { + if (device.payloadData() != null) { + payloadData = device.payloadData(); + break; + } + } + // Distribute payload to all devices with the same pseudo address + if (payloadData != null) { + // Share it amongst devices within advert refresh time limit + final long timeLimit = new Date().getTime() - BLESensorConfiguration.advertRefreshTimeInterval.millis(); + for (BLEDevice device : devices) { + if (device.payloadData() == null && device.createdAt.getTime() >= timeLimit) { + device.payloadData(payloadData); + } + } + } + // Get the most complete operating system + BLEDeviceOperatingSystem operatingSystem = null; + for (BLEDevice device : devices) { + if (device.operatingSystem() == BLEDeviceOperatingSystem.android || device.operatingSystem() == BLEDeviceOperatingSystem.ios) { + operatingSystem = device.operatingSystem(); + break; + } + } + // Distribute operating system to all devices with the same pseudo address + if (operatingSystem != null) { + for (BLEDevice device : devices) { + if (device.operatingSystem() == BLEDeviceOperatingSystem.unknown + || device.operatingSystem() == BLEDeviceOperatingSystem.android_tbc + || device.operatingSystem() == BLEDeviceOperatingSystem.ios_tbc) { + device.operatingSystem(operatingSystem); + } + } + } + return payloadData; + } + + + @Override + public BLEDevice device(BluetoothDevice bluetoothDevice) { + final TargetIdentifier identifier = new TargetIdentifier(bluetoothDevice); + BLEDevice device = database.get(identifier); + if (device == null) { + final BLEDevice newDevice = new BLEDevice(identifier, this); + device = newDevice; + database.put(identifier, newDevice); + queue.execute(new Runnable() { + @Override + public void run() { + logger.debug("create (device={})", identifier); + for (BLEDatabaseDelegate delegate : delegates) { + delegate.bleDatabaseDidCreate(newDevice); + } + } + }); + } + device.peripheral(bluetoothDevice); + return device; + } + + @Override + public BLEDevice device(PayloadData payloadData) { + BLEDevice device = null; + for (BLEDevice candidate : database.values()) { + if (payloadData.equals(candidate.payloadData())) { + device = candidate; + break; + } + } + if (device == null) { + final TargetIdentifier identifier = new TargetIdentifier(); + final BLEDevice newDevice = new BLEDevice(identifier, this); + device = newDevice; + database.put(identifier, newDevice); + queue.execute(new Runnable() { + @Override + public void run() { + logger.debug("create (device={})", identifier); + for (BLEDatabaseDelegate delegate : delegates) { + delegate.bleDatabaseDidCreate(newDevice); + } + } + }); + } + device.payloadData(payloadData); + return device; + } + + @Override + public List devices() { + return new ArrayList<>(database.values()); + } + + @Override + public void delete(final TargetIdentifier identifier) { + final BLEDevice device = database.remove(identifier); + if (device != null) { + queue.execute(new Runnable() { + @Override + public void run() { + logger.debug("delete (device={})", identifier); + for (final BLEDatabaseDelegate delegate : delegates) { + delegate.bleDatabaseDidDelete(device); + } + } + }); + } + } + + @Override + public PayloadSharingData payloadSharingData(final BLEDevice peer) { + final RSSI rssi = peer.rssi(); + if (rssi == null) { + return new PayloadSharingData(new RSSI(127), new Data(new byte[0])); + } + // Get other devices that were seen recently by this device + final List unknownDevices = new ArrayList<>(); + final List knownDevices = new ArrayList<>(); + for (BLEDevice device : database.values()) { + // Device was seen recently + if (device.timeIntervalSinceLastUpdate().value >= BLESensorConfiguration.payloadSharingExpiryTimeInterval.value) { + continue; + } + // Device has payload + if (device.payloadData() == null) { + continue; + } + // Device is iOS or receive only (Samsung J6) + if (!(device.operatingSystem() == BLEDeviceOperatingSystem.ios || device.receiveOnly())) { + continue; + } + // Payload is not the peer itself + if (peer.payloadData() != null && (Arrays.equals(device.payloadData().value, peer.payloadData().value))) { + continue; + } + // Payload is new to peer + if (peer.payloadSharingData.contains(device.payloadData())) { + knownDevices.add(device); + } else { + unknownDevices.add(device); + } + } + // Most recently seen unknown devices first + final List devices = new ArrayList<>(); + Collections.sort(unknownDevices, new Comparator() { + @Override + public int compare(BLEDevice d0, BLEDevice d1) { + return Long.compare(d1.lastUpdatedAt.getTime(), d0.lastUpdatedAt.getTime()); + } + }); + Collections.sort(knownDevices, new Comparator() { + @Override + public int compare(BLEDevice d0, BLEDevice d1) { + return Long.compare(d1.lastUpdatedAt.getTime(), d0.lastUpdatedAt.getTime()); + } + }); + devices.addAll(unknownDevices); + if (devices.size() == 0) { + return new PayloadSharingData(new RSSI(127), new Data(new byte[0])); + } + // Limit how much to share to avoid oversized data transfers over BLE + // (512 bytes limit according to spec, 510 with response, iOS requires response) + final Set sharedPayloads = new HashSet<>(devices.size()); + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + for (BLEDevice device : devices) { + final PayloadData payloadData = device.payloadData(); + if (payloadData == null) { + continue; + } + // Eliminate duplicates (this happens when the same device has changed address but the old version has not expired yet) + if (sharedPayloads.contains(payloadData)) { + continue; + } + // Limit payload sharing by BLE transfer limit + if (payloadData.value.length + byteArrayOutputStream.toByteArray().length > 510) { + break; + } + try { + byteArrayOutputStream.write(payloadData.value); + peer.payloadSharingData.add(payloadData); + sharedPayloads.add(payloadData); + } catch (Throwable e) { + logger.fault("Failed to append payload sharing data", e); + } + } + final Data data = new Data(byteArrayOutputStream.toByteArray()); + return new PayloadSharingData(rssi, data); + } + + // MARK:- BLEDeviceDelegate + + @Override + public void device(final BLEDevice device, final BLEDeviceAttribute didUpdate) { + queue.execute(new Runnable() { + @Override + public void run() { + logger.debug("update (device={},attribute={})", device.identifier, didUpdate.name()); + for (BLEDatabaseDelegate delegate : delegates) { + delegate.bleDatabaseDidUpdate(device, didUpdate); + } + } + }); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLEReceiver.kt b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLEReceiver.kt new file mode 100644 index 0000000..2bf6f0e --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLEReceiver.kt @@ -0,0 +1,961 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// +package au.gov.health.covidsafe.sensor.ble + +import android.bluetooth.* +import android.bluetooth.le.* +import android.content.Context +import android.os.Build +import android.os.ParcelUuid +import au.gov.health.covidsafe.app.TracerApp +import au.gov.health.covidsafe.bluetooth.gatt.WriteRequestPayload +import au.gov.health.covidsafe.factory.NetworkFactory +import au.gov.health.covidsafe.logging.CentralLog +import au.gov.health.covidsafe.sensor.SensorDelegate +import au.gov.health.covidsafe.sensor.ble.filter.BLEDeviceFilter +import au.gov.health.covidsafe.sensor.data.ConcreteSensorLogger +import au.gov.health.covidsafe.sensor.data.SensorLogger +import au.gov.health.covidsafe.sensor.datatype.* +import au.gov.health.covidsafe.streetpass.StreetPassPairingFix +import au.gov.health.covidsafe.streetpass.persistence.Encryption +import com.google.gson.GsonBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import java.util.* +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Executors +import kotlin.coroutines.CoroutineContext + +class ConcreteBLEReceiver(private val context: Context, private val bluetoothStateManager: BluetoothStateManager, timer: BLETimer, private val database: BLEDatabase, private val transmitter: BLETransmitter) : BluetoothGattCallback(), BLEReceiver, CoroutineScope { + private val logger: SensorLogger = ConcreteSensorLogger("Sensor", "BLE.ConcreteBLEReceiver") + private val operationQueue = Executors.newSingleThreadExecutor() + private val scanResults: Queue = ConcurrentLinkedQueue() + private val awsClient = NetworkFactory.awsClient + private val deviceFilter = BLEDeviceFilter() + + private enum class NextTask { + nothing, readPayload, writePayload, writeRSSI, writePayloadSharing + } + + //Result come here + private val scanCallback: ScanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, scanResult: ScanResult) { + logger.debug("onScanResult (result={})", scanResult) + scanResults.add(scanResult) + // Create or update device in database + val device = database.device(scanResult) + device.registerDiscovery() + // Read RSSI from scan result + device.rssi(RSSI(scanResult.rssi)) + } + + + override fun onBatchScanResults(results: List) { + for (scanResult in results) { + onScanResult(0, scanResult) + } + } + + override fun onScanFailed(errorCode: Int) { + logger.fault("onScanFailed (error={})", onScanFailedErrorCodeToString(errorCode)) + super.onScanFailed(errorCode) + } + } + + // MARK:- BLEReceiver + override fun add(delegate: SensorDelegate) { + BLEReceiver.delegates.add(delegate) + } + + override fun start() { + logger.debug("start") + // scanLoop is started by Bluetooth state + } + + override fun stop() { + logger.debug("stop") + // scanLoop is stopped by Bluetooth state + } + + // MARK:- Scan loop for startScan-wait-stopScan-processScanResults-wait-repeat + private enum class ScanLoopState { + scanStarting, scanStarted, scanStopping, scanStopped, processing, processed + } + + private inner class ScanLoopTask : BLETimerDelegate { + private var scanLoopState = ScanLoopState.processed + private var lastStateChangeAt = System.currentTimeMillis() + private fun state(now: Long, state: ScanLoopState) { + val elapsed = now - lastStateChangeAt + logger.debug("scanLoopTask, state change (from={},to={},elapsed={}ms)", scanLoopState, state, elapsed) + scanLoopState = state + lastStateChangeAt = now + } + + private fun timeSincelastStateChange(now: Long): Long { + return now - lastStateChangeAt + } + + private fun bluetoothLeScanner(): BluetoothLeScanner? { + val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + if (bluetoothAdapter == null) { + logger.fault("ScanLoop denied, Bluetooth adapter unavailable") + return null + } + val bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner + if (bluetoothLeScanner == null) { + logger.fault("ScanLoop denied, Bluetooth LE scanner unavailable") + return null + } + return bluetoothLeScanner + } + + override fun bleTimer(now: Long) { + when (scanLoopState) { + ScanLoopState.processed -> { + if (bluetoothStateManager.state() == BluetoothState.poweredOn) { + val period = timeSincelastStateChange(now) + if (period >= scanOffDurationMillis) { + logger.debug("scanLoopTask, start scan (process={}ms)", period) + val bluetoothLeScanner = bluetoothLeScanner() + if (bluetoothLeScanner == null) { + logger.fault("scanLoopTask, start scan denied, Bluetooth LE scanner unavailable") + return + } + state(now, ScanLoopState.scanStarting) + startScan(bluetoothLeScanner, Callback { value -> value?.let { state(now, if (value) ScanLoopState.scanStarted else ScanLoopState.scanStopped) } }) + } + } + return + } + ScanLoopState.scanStarted -> { + val period = timeSincelastStateChange(now) + if (period >= scanOnDurationMillis) { + logger.debug("scanLoopTask, stop scan (scan={}ms)", period) + val bluetoothLeScanner = bluetoothLeScanner() + if (bluetoothLeScanner == null) { + logger.fault("scanLoopTask, stop scan denied, Bluetooth LE scanner unavailable") + return + } + state(now, ScanLoopState.scanStopping) + stopScan(bluetoothLeScanner, Callback { value -> value?.let { state(now, ScanLoopState.scanStopped) } }) + } + return + } + ScanLoopState.scanStopped -> { + if (bluetoothStateManager.state() == BluetoothState.poweredOn) { + val period = timeSincelastStateChange(now) + if (period >= scanRestDurationMillis) { + logger.debug("scanLoopTask, start processing (stop={}ms)", period) + state(now, ScanLoopState.processing) + processScanResults(object : Callback { + override fun accept(value: Boolean?) { + value?.let { state(now, ScanLoopState.processed) } + } + }) + } + } + return + } + } + } + } + + /// Get BLE scanner and start scan + private fun startScan(bluetoothLeScanner: BluetoothLeScanner, callback: Callback?) { + logger.debug("startScan") + operationQueue.execute { + try { + scanForPeripherals(bluetoothLeScanner) + logger.debug("startScan successful") + callback?.let { callback.accept(true) } + } catch (e: Throwable) { + logger.fault("startScan failed", e) + callback?.let { callback.accept(false) } + } + } + } + + /// Scan for devices advertising sensor service and all Apple devices as + // iOS background advert does not include service UUID. There is a risk + // that the sensor will spend time communicating with Apple devices that + // are not running the sensor code repeatedly, but there is no reliable + // way of filtering this as the service may be absent only because of + // transient issues. This will be handled in taskConnect. + private fun scanForPeripherals(bluetoothLeScanner: BluetoothLeScanner) { + logger.debug("scanForPeripherals") + val filter: MutableList = ArrayList(2) + filter.add(ScanFilter.Builder().setManufacturerData( + BLESensorConfiguration.manufacturerIdForApple, ByteArray(0), ByteArray(0)).build()) + filter.add(ScanFilter.Builder().setServiceUuid( + ParcelUuid(BLESensorConfiguration.serviceUUID), + ParcelUuid(UUID(-0x1L, 0))) + .build()) + val settings = ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .setReportDelay(0) + .build() + bluetoothLeScanner.startScan(filter, settings, scanCallback) + } + + private fun processScanResults(callback: Callback) { + logger.debug("processScanResults") + operationQueue.execute { + try { + processScanResults() + logger.debug("processScanResults, processed scan results") + } catch (e: Throwable) { + logger.fault("processScanResults warning, processScanResults error", e) + callback.accept(false) + } + logger.debug("processScanResults successful") + callback.accept(true) + } + } + + /// Get BLE scanner and stop scan + private fun stopScan(bluetoothLeScanner: BluetoothLeScanner, callback: Callback) { + logger.debug("stopScan") + operationQueue.execute { + try { + bluetoothLeScanner.stopScan(scanCallback) + logger.debug("stopScan, stopped scanner") + } catch (e: Throwable) { + logger.fault("stopScan warning, bluetoothLeScanner.stopScan error", e) + } + try { + val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + bluetoothAdapter?.cancelDiscovery() + logger.debug("stopScan, cancelled discovery") + } catch (e: Throwable) { + logger.fault("stopScan warning, bluetoothAdapter.cancelDiscovery error", e) + } + logger.debug("stopScan successful") + callback.accept(true) + } + } + + // MARK:- Process scan results + /// Process scan results. + private fun processScanResults() { + val t0 = System.currentTimeMillis() + logger.debug("processScanResults (results={})", scanResults.size) + // Identify devices discovered in last scan + val didDiscover = didDiscover() + taskRemoveExpiredDevices() + taskCorrectConnectionStatus() + taskConnect(didDiscover) + val t1 = System.currentTimeMillis() + logger.debug("processScanResults (results={},devices={},elapsed={}ms)", scanResults.size, didDiscover.size, t1 - t0) + } + // MARK:- didDiscover + /** + * Process scan results to ... + * 1. Create BLEDevice from scan result for new devices + * 2. Read RSSI + * 3. Identify operating system where possible + */ + private fun didDiscover(): List { + // Take current copy of concurrently modifiable scan results + val scanResultList: MutableList = ArrayList(scanResults.size) + while (scanResults.size > 0) { + scanResultList.add(scanResults.poll()) + } + + // Process scan results and return devices created/updated in scan results + logger.debug("didDiscover (scanResults={})", scanResultList.size) + val deviceSet: MutableSet = HashSet() + val devices: MutableList = ArrayList() + for (scanResult in scanResultList) { + val device = database.device(scanResult) + if (deviceSet.add(device)) { + logger.debug("didDiscover (device={})", device) + devices.add(device) + } + // Set scan record + device.scanRecord(scanResult.scanRecord) + // Set TX power level + if (device.scanRecord() != null) { + val txPowerLevel = device.scanRecord().txPowerLevel + if (txPowerLevel != Int.MIN_VALUE) { + device.txPower(BLE_TxPower(txPowerLevel)) + } + } + // Identify operating system from scan record where possible + // - Sensor service found + Manufacturer is Apple -> iOS (Foreground) + // - Sensor service found + Manufacturer not Apple -> Android + // - Sensor service not found + Manufacturer is Apple -> iOS (Background) or Apple device not advertising sensor service, to be resolved later + // - Sensor service not found + Manufacturer not Apple -> Ignore (shouldn't be possible as we are scanning for Apple or with service) + val hasSensorService = hasSensorService(scanResult) + val isAppleDevice = isAppleDevice(scanResult) + if (hasSensorService && isAppleDevice) { + // Definitely iOS device offering sensor service in foreground mode + device.operatingSystem(BLEDeviceOperatingSystem.ios) + } else if (hasSensorService) { // !isAppleDevice implied + // Definitely Android device offering sensor service + if (device.operatingSystem() != BLEDeviceOperatingSystem.android) { + device.operatingSystem(BLEDeviceOperatingSystem.android_tbc) + } + } else if (isAppleDevice) { // !hasSensorService implied + // Filter device by advert messages unless it is already confirmed ios device + val matchingPattern: BLEDeviceFilter.MatchingPattern? = deviceFilter.match(device) + if (device.operatingSystem() !== BLEDeviceOperatingSystem.ios && matchingPattern != null) { + logger.fault("didDiscover, ignoring filtered device (device={},pattern={},message={})", device, matchingPattern.filterPattern.regularExpression, matchingPattern.message) + device.operatingSystem(BLEDeviceOperatingSystem.ignore) + } + // Possibly an iOS device offering sensor service in background mode, + // can't be sure without additional checks after connection, so + // only set operating system if it is unknown to offer a guess. + if (device.operatingSystem() == BLEDeviceOperatingSystem.unknown) { + device.operatingSystem(BLEDeviceOperatingSystem.ios_tbc) + } + } else { + // Sensor service not found + Manufacturer not Apple should be impossible + // as we are scanning for devices with sensor service or Apple device. + logger.fault("didDiscover, invalid non-Apple device without sensor service (device={})", device) + if (!(device.operatingSystem() == BLEDeviceOperatingSystem.ios || device.operatingSystem() == BLEDeviceOperatingSystem.android)) { + device.operatingSystem(BLEDeviceOperatingSystem.ignore) + } + } + } + return devices + } + + // MARK:- House keeping tasks + /// Remove devices that have not been updated for over 15 minutes, as the UUID + // is likely to have changed after being out of range for over 20 minutes, + // so it will require discovery. Discovery is fast and cheap on Android. + private fun taskRemoveExpiredDevices() { + val devicesToRemove: MutableList = ArrayList() + for (device in database.devices()) { + if (device.timeIntervalSinceLastUpdate().value > TimeInterval.minutes(15).value) { + devicesToRemove.add(device) + } + } + for (device in devicesToRemove) { + logger.debug("taskRemoveExpiredDevices (remove={})", device) + database.delete(device.identifier) + } + } + + /// Connections should not be held for more than 1 minute, likely to have not received onConnectionStateChange callback. + private fun taskCorrectConnectionStatus() { + for (device in database.devices()) { + if (device.state() == BLEDeviceState.connected && device.timeIntervalSinceConnected().value > TimeInterval.minute.value) { + logger.debug("taskCorrectConnectionStatus (device={})", device) + device.state(BLEDeviceState.disconnected) + } + } + } + + // MARK:- Connect task + private fun taskConnect(discovered: List) { + // Clever connection prioritisation is pointless here as devices + // like the Samsung A10 and A20 changes mac address on every scan + // call, so optimising new device handling is more effective. + val timeStart = System.currentTimeMillis() + var devicesProcessed = 0 + for (device in discovered) { + // Stop process if exceeded time limit + val elapsedTime = System.currentTimeMillis() - timeStart + if (elapsedTime >= scanProcessDurationMillis) { + logger.debug("taskConnect, reached time limit (elapsed={}ms,limit={}ms)", elapsedTime, scanProcessDurationMillis) + break + } + if (devicesProcessed > 0) { + val predictedElapsedTime = Math.round(elapsedTime / devicesProcessed.toDouble() * (devicesProcessed + 1)) + if (predictedElapsedTime > scanProcessDurationMillis) { + logger.debug("taskConnect, likely to exceed time limit soon (elapsed={}ms,devicesProcessed={},predicted={}ms,limit={}ms)", elapsedTime, devicesProcessed, predictedElapsedTime, scanProcessDurationMillis) + break + } + } + if (nextTaskForDevice(device) == NextTask.nothing) { + logger.debug("taskConnect, no pending action (device={})", device) + continue + } + taskConnectDevice(device) + devicesProcessed++ + } + } + + private fun taskConnectDevice(device: BLEDevice) { + if (device.state() == BLEDeviceState.connected) { + logger.debug("taskConnectDevice, already connected to transmitter (device={})", device) + return + } + // Connect (timeout at 95% = 2 SD) + val timeConnect = System.currentTimeMillis() + logger.debug("taskConnectDevice, connect (device={})", device) + device.state(BLEDeviceState.connecting) + //val gatt = device.peripheral().connectGatt(context, false, this) + val gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + device.peripheral().connectGatt(context, false, this, BluetoothDevice.TRANSPORT_LE) + } else { + // use reflection to call connectGatt(Context context, boolean autoConnect, BluetoothGattCallback callback, int transport) + try { + device.javaClass.getMethod( + "connectGatt", Context::class.java, Boolean::class.java, BluetoothGattCallback::class.java, Int::class.java + ).invoke( + // BluetoothDevice.TRANSPORT_LE = 2 + device, context, false, this, 2) as BluetoothGatt + } catch (e: Exception) { + logger.fault("Reflection call of connectGatt() failed.", e) + // reflection failed; call connectGatt(Context context, boolean autoConnect, BluetoothGattCallback callback) instead + device.peripheral().connectGatt(context, false, this) + } + } + if (gatt == null) { + logger.fault("taskConnectDevice, connect failed (device={})", device) + device.state(BLEDeviceState.disconnected) + return + } + // Wait for connection + while (device.state() != BLEDeviceState.connected && device.state() != BLEDeviceState.disconnected && System.currentTimeMillis() - timeConnect < timeToConnectDeviceLimitMillis) { + try { + Thread.sleep(200) + } catch (e: Throwable) { + logger.fault("Timer interrupted", e) + } + } + if (device.state() != BLEDeviceState.connected) { + logger.fault("taskConnectDevice, connect timeout (device={})", device) + try { + gatt.close() + } catch (e: Throwable) { + logger.fault("taskConnectDevice, close failed (device={})", device, e) + } + return + } else { + val connectElapsed = System.currentTimeMillis() - timeConnect + // Add sample to adaptive connection timeout + timeToConnectDevice.add(connectElapsed.toDouble()) + logger.debug("taskConnectDevice, connected (device={},elapsed={}ms,statistics={})", device, connectElapsed, timeToConnectDevice) + } + // Wait for disconnection + while (device.state() != BLEDeviceState.disconnected && System.currentTimeMillis() - timeConnect < scanProcessDurationMillis) { + try { + Thread.sleep(500) + } catch (e: Throwable) { + logger.fault("Timer interrupted", e) + } + } + var success = true + // Timeout connection if required, and always set state to disconnected + if (device.state() != BLEDeviceState.disconnected) { + logger.fault("taskConnectDevice, disconnect timeout (device={})", device) + try { + gatt.close() + } catch (e: Throwable) { + logger.fault("taskConnectDevice, close failed (device={})", device, e) + } + success = false + } + device.state(BLEDeviceState.disconnected) + val timeDisconnect = System.currentTimeMillis() + val timeElapsed = timeDisconnect - timeConnect + if (success) { + timeToProcessDevice.add(timeElapsed.toDouble()) + logger.debug("taskConnectDevice, complete (success=true,device={},elapsed={}ms,statistics={})", device, timeElapsed, timeToProcessDevice) + } else { + logger.fault("taskConnectDevice, complete (success=false,device={},elapsed={}ms)", device, timeElapsed) + } + } + + // MARK:- BluetoothStateManagerDelegate + override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { + val device = database.device(gatt.device) + logger.debug("onConnectionStateChange (device={},status={},state={})", device, bleStatus(status), bleState(newState)) + if (newState == BluetoothProfile.STATE_CONNECTED) { + device.state(BLEDeviceState.connected) + gatt.discoverServices() + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + gatt.close() + device.state(BLEDeviceState.disconnected) + if (status != 0) { + if (!(device.operatingSystem() == BLEDeviceOperatingSystem.ios || device.operatingSystem() == BLEDeviceOperatingSystem.android)) { + device.operatingSystem(BLEDeviceOperatingSystem.ignore) + } + } + } else { + logger.debug("onConnectionStateChange (device={},status={},state={})", device, bleStatus(status), bleState(newState)) + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { + val device = database.device(gatt.device) + logger.debug("onServicesDiscovered (device={},status={})", device, bleStatus(status)) + val service = gatt.getService(BLESensorConfiguration.serviceUUID) + if (service == null) { + logger.fault("onServicesDiscovered, missing sensor service (device={})", device) + // Ignore device for a while unless it is a confirmed iOS or Android device + if (!(device.operatingSystem() == BLEDeviceOperatingSystem.ios || device.operatingSystem() == BLEDeviceOperatingSystem.android)) { + device.operatingSystem(BLEDeviceOperatingSystem.ignore) + } + gatt.disconnect() + return + } + logger.debug("onServicesDiscovered, found sensor service (device={})", device) + device.invalidateCharacteristics() + var readService: Boolean = false + for (characteristic in service.characteristics) { + // Confirm operating system with signal characteristic + if (characteristic.uuid == BLESensorConfiguration.androidSignalCharacteristicUUID) { + logger.debug("onServicesDiscovered, found Android signal characteristic (device={})", device) + device.operatingSystem(BLEDeviceOperatingSystem.android) + device.signalCharacteristic(characteristic) + } else if (characteristic.uuid == BLESensorConfiguration.iosSignalCharacteristicUUID) { + logger.debug("onServicesDiscovered, found iOS signal characteristic (device={})", device) + device.operatingSystem(BLEDeviceOperatingSystem.ios) + device.signalCharacteristic(characteristic) + } else if (characteristic.uuid == BLESensorConfiguration.payloadCharacteristicUUID) { + logger.debug("onServicesDiscovered, found payload characteristic (device={})", device) + device.payloadCharacteristic(characteristic) + readService = true + } else if (characteristic.uuid == BLESensorConfiguration.legacyCovidsafePayloadCharacteristicUUID && !readService) { + logger.debug("onServicesDiscovered, found covidsafe legacy payload characteristic (device={})", device) + //If they have the legacy characteristic we know it a COVID app and can set the OS to be confirmed + if(device.operatingSystem() == BLEDeviceOperatingSystem.android_tbc) { + device.operatingSystem(BLEDeviceOperatingSystem.android) + }else if(device.operatingSystem() == BLEDeviceOperatingSystem.ios_tbc) { + device.operatingSystem(BLEDeviceOperatingSystem.ios) + } + device.payloadCharacteristic(characteristic) + device.legacyPayloadCharacteristic(characteristic) + } + } + nextTask(gatt) + } + + private fun nextTaskForDevice(device: BLEDevice): NextTask { + // No task for devices marked as .ignore + if (device.ignore()) { + return NextTask.nothing + } + // If marked as ignore but ignore has expired, change to unknown + if (device.operatingSystem() == BLEDeviceOperatingSystem.ignore) { + logger.debug("nextTaskForDevice, switching ignore to unknown (device={},reason=ignoreExpired)", device) + device.operatingSystem(BLEDeviceOperatingSystem.unknown) + } + // No task for devices marked as receive only (no advert to connect to) + if (device.receiveOnly()) { + return NextTask.nothing + } + // Resolve or confirm operating system by reading payload which + // triggers characteristic discovery to confirm the operating system + if (device.operatingSystem() == BLEDeviceOperatingSystem.unknown || + device.operatingSystem() == BLEDeviceOperatingSystem.ios_tbc) { + logger.debug("nextTaskForDevice (device={},task=readPayload|OS)", device) + return NextTask.readPayload + } + // Get payload as top priority + if (device.payloadData() == null) { + logger.debug("nextTaskForDevice (device={},task=readPayload)", device) + return NextTask.readPayload + } + if (device.timeIntervalSinceLastPayloadUpdate().millis() > payloadDataUpdateTimeInterval) { + logger.debug("nextTaskForDevice (device={},task=readPayloadUpdate)", device) + return NextTask.readPayload + } + + // Write payload, rssi and payload sharing data if this device cannot transmit + if (!transmitter.isSupported) { + // Write payload data as top priority + if (device.timeIntervalSinceLastWritePayload().value > TimeInterval.seconds(30).value) { + logger.debug("nextTaskForDevice (device={},task=writePayload,elapsed={})", device, device.timeIntervalSinceLastWritePayload()) + return NextTask.writePayload + } + // Write payload sharing data to iOS device if there is data to be shared (alternate between payload sharing and write RSSI) + val payloadSharingData = database.payloadSharingData(device) + if (device.operatingSystem() == BLEDeviceOperatingSystem.ios + && payloadSharingData.data.value.size > 0 + && device.timeIntervalSinceLastWritePayloadSharing().value >= TimeInterval.seconds(15).value + && device.timeIntervalSinceLastWritePayloadSharing().value >= device.timeIntervalSinceLastWriteRssi().value) { + logger.debug("nextTaskForDevice (device={},task=writePayloadSharing,dataLength={},elapsed={})", device, payloadSharingData.data.value.size, + device.timeIntervalSinceLastWritePayloadSharing()) + return NextTask.writePayloadSharing + } + // Write RSSI as frequently as reasonable + if (device.rssi() != null + && device.timeIntervalSinceLastWriteRssi().value >= TimeInterval.seconds(15).value + && (device.timeIntervalSinceLastWritePayload().millis() < payloadDataUpdateTimeInterval + || device.timeIntervalSinceLastWriteRssi().value >= device.timeIntervalSinceLastWritePayload().value)) { + logger.debug("nextTaskForDevice (device={},task=writeRSSI,elapsed={})", device, device.timeIntervalSinceLastWriteRssi()) + return NextTask.writeRSSI + } + // Write payload update if required + if (device.timeIntervalSinceLastWritePayload().millis() > payloadDataUpdateTimeInterval) { + logger.debug("nextTaskForDevice (device={},task=writePayloadUpdate,elapsed={})", device, device.timeIntervalSinceLastWritePayload()); + return NextTask.writePayload; + } + } else if (device.legacyPayloadCharacteristic != null) { + if (device.timeIntervalSinceLastWritePayload().value > TimeInterval.seconds(30).value) { + logger.debug("nextTaskForDevice (device={},task=writePayload,elapsed={})", device, device.timeIntervalSinceLastWritePayload()) + return NextTask.writePayload + } + } + // Write payload sharing data to iOS + if (device.operatingSystem() == BLEDeviceOperatingSystem.ios) { + // Write payload sharing data to iOS device if there is data to be shared + val payloadSharingData = database.payloadSharingData(device) + if (device.operatingSystem() == BLEDeviceOperatingSystem.ios && payloadSharingData.data.value.size > 0 && device.timeIntervalSinceLastWritePayloadSharing().value >= TimeInterval.seconds(15).value) { + logger.debug("nextTaskForDevice (device={},task=writePayloadSharing,dataLength={},elapsed={})", device, payloadSharingData.data.value.size, device.timeIntervalSinceLastWritePayloadSharing()) + return NextTask.writePayloadSharing + } + } + return NextTask.nothing + } + + private fun nextTask(gatt: BluetoothGatt) { + val device = database.device(gatt.device) + val nextTask = nextTaskForDevice(device) + when (nextTask) { + NextTask.readPayload -> { + val payloadCharacteristic = device.payloadCharacteristic() + + if (payloadCharacteristic == null) { + logger.fault("nextTask failed (task=readPayload,device={},reason=missingPayloadCharacteristic)", device) + gatt.disconnect() + return // => onConnectionStateChange + } + StreetPassPairingFix.bypassAuthenticationRetry(gatt); + if (!gatt.readCharacteristic(payloadCharacteristic)) { + logger.fault("nextTask failed (task=readPayload,device={},reason=readCharacteristicFailed)", device) + gatt.disconnect() + return // => onConnectionStateChange + } + logger.debug("nextTask (task=readPayload,device={})", device) + return // => onCharacteristicRead | timeout + } + NextTask.writePayload -> { + val payloadData = transmitter.payloadData() + if (payloadData == null || payloadData.value == null || payloadData.value.size == 0) { + logger.fault("nextTask failed (task=writePayload,device={},reason=missingPayloadData)", device) + gatt.disconnect() + return // => onConnectionStateChange + } + var data = SignalCharacteristicData.encodeWritePayload(transmitter.payloadData()) + if (device.legacyPayloadCharacteristic != null) { + val legacyPayload = getWritePayloadForLegacyCentral(device) + if (legacyPayload != null) { + data = Data(legacyPayload.getPayload()) + } + } + logger.debug("nextTask (task=writePayload,device={},dataLength={})", device, data.value.size) + writeSignalCharacteristic(gatt, NextTask.writePayload, data.value) + return + } + NextTask.writePayloadSharing -> { + val payloadSharingData = database.payloadSharingData(device) + if (payloadSharingData == null) { + logger.fault("nextTask failed (task=writePayloadSharing,device={},reason=missingPayloadSharingData)", device) + gatt.disconnect() + return + } + val data = SignalCharacteristicData.encodeWritePayloadSharing(payloadSharingData) + logger.debug("nextTask (task=writePayloadSharing,device={},dataLength={})", device, data.value.size) + writeSignalCharacteristic(gatt, NextTask.writePayloadSharing, data.value) + return + } + NextTask.writeRSSI -> { + val signalCharacteristic = device.signalCharacteristic() + if (signalCharacteristic == null) { + logger.fault("nextTask failed (task=writeRSSI,device={},reason=missingSignalCharacteristic)", device) + gatt.disconnect() + return + } + val rssi = device.rssi() + if (rssi == null) { + logger.fault("nextTask failed (task=writeRSSI,device={},reason=missingRssiData)", device) + gatt.disconnect() + return + } + val data = SignalCharacteristicData.encodeWriteRssi(rssi) + logger.debug("nextTask (task=writeRSSI,device={},dataLength={})", device, data.value.size) + writeSignalCharacteristic(gatt, NextTask.writeRSSI, data.value) + return + } + } + logger.debug("nextTask (task=nothing,device={})", device) + gatt.disconnect() + } + + inner class EncryptedWriteRequestPayload(val timestamp: Long, val modelC: String, val rssi: Int, val txPower: Int?, val msg: String?) + + private fun getWritePayloadForLegacyCentral(device: BLEDevice): WriteRequestPayload? { + val thisCentralDevice = TracerApp.asCentralDevice() + val gson = GsonBuilder().disableHtmlEscaping().create() + + val DUMMY_DEVICE = "" + val DUMMY_RSSI = 999 + val DUMMY_TXPOWER = 999 + + val rssi = if (device.rssi() != null) device.rssi().value else return null + val txPower = if (device.txPower() != null) device.txPower().value else DUMMY_TXPOWER + + val plainRecord = gson.toJson(EncryptedWriteRequestPayload( + System.currentTimeMillis() / 1000L, + thisCentralDevice.modelC, + rssi, + txPower, + TracerApp.thisDeviceMsg())) + + CentralLog.d("BLEReceiver", "onCharacteristicRead plainRecord = $plainRecord") + + val remoteBlob = Encryption.encryptPayload(plainRecord.toByteArray(Charsets.UTF_8)) + + val writedata = WriteRequestPayload( + v = TracerApp.protocolVersion, + msg = remoteBlob, + org = TracerApp.ORG, + modelC = DUMMY_DEVICE, + rssi = DUMMY_RSSI, + txPower = DUMMY_TXPOWER + ) + CentralLog.d("BLEReceiver", "writedata = $writedata") + return writedata + } + + private fun writeSignalCharacteristic(gatt: BluetoothGatt, task: NextTask, data: ByteArray?) { + val device = database.device(gatt.device) + val signalCharacteristic = device.signalCharacteristic() + if (signalCharacteristic == null) { + // if no signal characteristic is most likely to be legacy and we try to write + if (device.legacyPayloadCharacteristic != null) { + val legacyCharacteristic = device.legacyPayloadCharacteristic + + logger.debug("writeSignalCharacteristic for Legacy") + legacyCharacteristic.value = data + legacyCharacteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + StreetPassPairingFix.bypassAuthenticationRetry(gatt) + if (!gatt.writeCharacteristic(legacyCharacteristic)) { + logger.fault("writeLegacyCharacteristic failed (task={}},device={},reason=writeCharacteristicFailed)", task, device) + gatt.disconnect() + } else { + logger.debug("writeLegacyCharacteristic (task={},dataLength={},device={})", task, data?.size, device) + // => onCharacteristicWrite + } + } else { + logger.fault("writeSignalCharacteristic failed (task={},device={},reason=missingSignalCharacteristic)", task, device) + gatt.disconnect() + } + return + } + if (data == null || data.size == 0) { + logger.fault("writeSignalCharacteristic failed (task={},device={},reason=missingData)", task, device) + gatt.disconnect() + return + } + if (signalCharacteristic.uuid == BLESensorConfiguration.iosSignalCharacteristicUUID) { + device.signalCharacteristicWriteValue = data + device.signalCharacteristicWriteQueue = null + signalCharacteristic.value = data + signalCharacteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + StreetPassPairingFix.bypassAuthenticationRetry(gatt) + if (!gatt.writeCharacteristic(signalCharacteristic)) { + logger.fault("writeSignalCharacteristic to iOS failed (task={}},device={},reason=writeCharacteristicFailed)", task, device) + gatt.disconnect() + } else { + logger.debug("writeSignalCharacteristic to iOS (task={},dataLength={},device={})", task, data.size, device) + // => onCharacteristicWrite + } + return + } + if (signalCharacteristic.uuid == BLESensorConfiguration.androidSignalCharacteristicUUID) { + device.signalCharacteristicWriteValue = data + device.signalCharacteristicWriteQueue = fragmentDataByMtu(data) + if (writeAndroidSignalCharacteristic(gatt) == WriteAndroidSignalCharacteristicResult.failed) { + logger.fault("writeSignalCharacteristic to Android failed (task={}},device={},reason=writeCharacteristicFailed)", task, device) + gatt.disconnect() + } else { + logger.debug("writeSignalCharacteristic to Android (task={},dataLength={},device={})", task, data.size, device) + // => onCharacteristicWrite + } + } + } + + private enum class WriteAndroidSignalCharacteristicResult { + moreToWrite, complete, failed + } + + private fun writeAndroidSignalCharacteristic(gatt: BluetoothGatt): WriteAndroidSignalCharacteristicResult { + val device = database.device(gatt.device) + val signalCharacteristic = device.signalCharacteristic() + if (signalCharacteristic == null) { + logger.fault("writeAndroidSignalCharacteristic failed (device={},reason=missingSignalCharacteristic)", device) + return WriteAndroidSignalCharacteristicResult.failed + } + if (device.signalCharacteristicWriteQueue == null || device.signalCharacteristicWriteQueue.size == 0) { + logger.debug("writeAndroidSignalCharacteristic completed (device={})", device) + return WriteAndroidSignalCharacteristicResult.complete + } + logger.debug("writeAndroidSignalCharacteristic (device={},queue={})", device, device.signalCharacteristicWriteQueue.size) + val data = device.signalCharacteristicWriteQueue.poll() + signalCharacteristic.value = data + signalCharacteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + StreetPassPairingFix.bypassAuthenticationRetry(gatt) + return if (!gatt.writeCharacteristic(signalCharacteristic)) { + logger.fault("writeAndroidSignalCharacteristic failed (device={},reason=writeCharacteristicFailed)", device) + WriteAndroidSignalCharacteristicResult.failed + } else { + logger.debug("writeAndroidSignalCharacteristic (device={},remaining={})", device, device.signalCharacteristicWriteQueue.size) + WriteAndroidSignalCharacteristicResult.moreToWrite + } + } + + /// Split data into fragments, where each fragment has length <= mtu + private fun fragmentDataByMtu(data: ByteArray): Queue { + val fragments: Queue = ConcurrentLinkedQueue() + var i = 0 + while (i < data.size) { + val fragment = ByteArray(Math.min(defaultMTU, data.size - i)) + System.arraycopy(data, i, fragment, 0, fragment.size) + fragments.add(fragment) + i += defaultMTU + } + return fragments + } + + override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { + val device = database.device(gatt.device) + val success = status == BluetoothGatt.GATT_SUCCESS + logger.debug("onCharacteristicRead (device={},status={})", device, bleStatus(status)) + if (characteristic.uuid == BLESensorConfiguration.payloadCharacteristicUUID || characteristic.uuid == BLESensorConfiguration.legacyCovidsafePayloadCharacteristicUUID) { + val payloadData = if (characteristic.value != null) PayloadData(characteristic.value) else null + if (success) { + if (payloadData != null) { + logger.debug("onCharacteristicRead, read payload data success (device={},payload={})", device, payloadData.shortName()) + device.payloadData(payloadData) + } else { + logger.fault("onCharacteristicRead, read payload data failed, no data (device={})", device) + } + } else { + logger.fault("onCharacteristicRead, read payload data failed (device={})", device) + } + } + nextTask(gatt) + } + + override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { + val device = database.device(gatt.device) + logger.debug("onCharacteristicWrite (device={},status={})", device, bleStatus(status)) + val signalCharacteristic = device.signalCharacteristic() + val success = status == BluetoothGatt.GATT_SUCCESS + if (signalCharacteristic.uuid == BLESensorConfiguration.androidSignalCharacteristicUUID) { + if (success && writeAndroidSignalCharacteristic(gatt) == WriteAndroidSignalCharacteristicResult.moreToWrite) { + return + } + } + val signalCharacteristicDataType = SignalCharacteristicData.detect(Data(device.signalCharacteristicWriteValue)) + signalCharacteristic.value = ByteArray(0) + device.signalCharacteristicWriteValue = null + device.signalCharacteristicWriteQueue = null + when (signalCharacteristicDataType) { + SignalCharacteristicDataType.payload -> { + if (success) { + logger.debug("onCharacteristicWrite, write payload success (device={})", device) + device.registerWritePayload() + } else { + logger.fault("onCharacteristicWrite, write payload failed (device={})", device) + } + return + } + SignalCharacteristicDataType.rssi -> { + if (success) { + logger.debug("onCharacteristicWrite, write RSSI success (device={})", device) + device.registerWriteRssi() + } else { + logger.fault("onCharacteristicWrite, write RSSI failed (device={})", device) + } + return + } + SignalCharacteristicDataType.payloadSharing -> { + if (success) { + logger.debug("onCharacteristicWrite, write payload sharing success (device={})", device) + device.registerWritePayloadSharing() + } else { + logger.fault("onCharacteristicWrite, write payload sharing failed (device={})", device) + } + return + } + else -> { + logger.fault("onCharacteristicWrite, write unknown data (device={},success={})", device, success) + return + } + } + nextTask(gatt) + } + + companion object { + // Scan ON/OFF/PROCESS durations + private val scanOnDurationMillis = TimeInterval.seconds(4).millis() + private val scanRestDurationMillis = TimeInterval.seconds(1).millis() + private val scanProcessDurationMillis = TimeInterval.seconds(60).millis() + private val scanOffDurationMillis = TimeInterval.seconds(2).millis() + private val timeToConnectDeviceLimitMillis = TimeInterval.seconds(12).millis() + private val timeToConnectDevice = Sample() + private val timeToProcessDevice = Sample() + private const val defaultMTU = 20 + private val payloadDataUpdateTimeInterval = TimeInterval.minutes(5).millis() + + /// Does scan result include advert for sensor service? + private fun hasSensorService(scanResult: ScanResult): Boolean { + val scanRecord = scanResult.scanRecord ?: return false + val serviceUuids = scanRecord.serviceUuids + if (serviceUuids == null || serviceUuids.size == 0) { + return false + } + for (serviceUuid in serviceUuids) { + if (serviceUuid.uuid == BLESensorConfiguration.serviceUUID) { + return true + } + } + return false + } + + /// Does scan result indicate device was manufactured by Apple? + private fun isAppleDevice(scanResult: ScanResult): Boolean { + val scanRecord = scanResult.scanRecord ?: return false + val data = scanRecord.getManufacturerSpecificData(BLESensorConfiguration.manufacturerIdForApple) + return data != null + } + + // MARK:- Bluetooth code transformers + private fun bleStatus(status: Int): String { + return if (status == BluetoothGatt.GATT_SUCCESS) { + "GATT_SUCCESS" + } else { + "GATT_FAILURE" + } + } + + private fun bleState(state: Int): String { + return when (state) { + BluetoothProfile.STATE_CONNECTED -> "STATE_CONNECTED" + BluetoothProfile.STATE_DISCONNECTED -> "STATE_DISCONNECTED" + else -> "UNKNOWN_STATE_$state" + } + } + + private fun onScanFailedErrorCodeToString(errorCode: Int): String { + return when (errorCode) { + ScanCallback.SCAN_FAILED_ALREADY_STARTED -> "SCAN_FAILED_ALREADY_STARTED" + ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED" + ScanCallback.SCAN_FAILED_INTERNAL_ERROR -> "SCAN_FAILED_INTERNAL_ERROR" + ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED -> "SCAN_FAILED_FEATURE_UNSUPPORTED" + else -> "UNKNOWN_ERROR_CODE_$errorCode" + } + } + } + + /** + * Receiver starts automatically when Bluetooth is enabled. + */ + init { + timer.add(ScanLoopTask()) + } + private var job: Job = Job() + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + job +} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLESensor.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLESensor.java new file mode 100644 index 0000000..ad28356 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLESensor.java @@ -0,0 +1,194 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +import android.content.Context; +import android.util.Log; + +import com.atlassian.mobilekit.module.feedback.commands.SendFeedbackCommand; + +import au.gov.health.covidsafe.sensor.SensorDelegate; +import au.gov.health.covidsafe.sensor.data.ConcreteSensorLogger; +import au.gov.health.covidsafe.sensor.data.SensorLogger; +import au.gov.health.covidsafe.sensor.datatype.BluetoothState; +import au.gov.health.covidsafe.sensor.datatype.PayloadData; +import au.gov.health.covidsafe.sensor.datatype.Proximity; +import au.gov.health.covidsafe.sensor.datatype.ProximityMeasurementUnit; +import au.gov.health.covidsafe.sensor.datatype.RSSI; +import au.gov.health.covidsafe.sensor.datatype.SensorState; +import au.gov.health.covidsafe.sensor.datatype.SensorType; +import au.gov.health.covidsafe.sensor.datatype.TimeInterval; +import au.gov.health.covidsafe.sensor.payload.PayloadDataSupplier; + +import java.util.Date; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ConcreteBLESensor implements BLESensor, BLEDatabaseDelegate, BluetoothStateManagerDelegate { + private final SensorLogger logger = new ConcreteSensorLogger("Sensor", "BLE.ConcreteBLESensor"); + private final Queue delegates = new ConcurrentLinkedQueue<>(); + private final BLETransmitter transmitter; + private final BLEReceiver receiver; + private final ExecutorService operationQueue = Executors.newSingleThreadExecutor(); + private static final String LOG_TAG = ConcreteBLESensor.class.getSimpleName(); + + // Record payload data to enable de-duplication + private final Map didReadPayloadData = new ConcurrentHashMap<>(); + + public ConcreteBLESensor(Context context, PayloadDataSupplier payloadDataSupplier) { + final BluetoothStateManager bluetoothStateManager = new ConcreteBluetoothStateManager(context); + final BLEDatabase database = new ConcreteBLEDatabase(); + final BLETimer timer = new BLETimer(context); + bluetoothStateManager.delegates.add(this); + transmitter = new ConcreteBLETransmitter(context, bluetoothStateManager, timer, payloadDataSupplier, database); + receiver = new ConcreteBLEReceiver(context, bluetoothStateManager, timer, database, transmitter); + database.add(this); + } + + @Override + public void add(SensorDelegate delegate) { + delegates.add(delegate); + transmitter.add(delegate); + receiver.add(delegate); + } + + @Override + public void start() { + logger.debug("start"); + // BLE transmitter and receivers start on powerOn event + transmitter.start(); + receiver.start(); + } + + @Override + public void stop() { + logger.debug("stop"); + // BLE transmitter and receivers stops on powerOff event + transmitter.stop(); + receiver.stop(); + } + + // MARK:- BLEDatabaseDelegate + + @Override + public void bleDatabaseDidCreate(final BLEDevice device) { + logger.debug("didDetect (device={},payloadData={})", device.identifier, device.payloadData()); + operationQueue.execute(new Runnable() { + @Override + public void run() { + for (SensorDelegate delegate : delegates) { + delegate.sensor(SensorType.BLE, device.identifier); + } + } + }); + } + + @Override + public void bleDatabaseDidUpdate(final BLEDevice device, BLEDeviceAttribute attribute) { + //Save in database here + Log.d(LOG_TAG, "attribute:" + attribute); + Log.d(LOG_TAG, "device:" + device); + switch (attribute) { + case rssi: { + final RSSI rssi = device.rssi(); + if (rssi == null) { + return; + } + final Proximity proximity = new Proximity(ProximityMeasurementUnit.RSSI, (double) rssi.value); + Log.d(LOG_TAG, "device.payloadData():" + device.payloadData()); + Log.d(LOG_TAG, "device:" + device); + Log.d(LOG_TAG, "proximity.description():" + proximity.description()); + // We receive payload here + logger.debug("didMeasure (device={},payloadData={},proximity={})", device, device.payloadData(), proximity.description()); + operationQueue.execute(new Runnable() { + @Override + public void run() { + for (SensorDelegate delegate : delegates) { + delegate.sensor(SensorType.BLE, proximity, device.identifier); + } + } + }); + final PayloadData payloadData = device.payloadData(); + if (payloadData == null) { + return; + } + operationQueue.execute(new Runnable() { + @Override + public void run() { + for (SensorDelegate delegate : delegates) { + delegate.sensor(SensorType.BLE, proximity, device.identifier, payloadData,device); + } + } + }); + break; + } + case payloadData: { + final PayloadData payloadData = device.payloadData(); + if (payloadData == null) { + return; + } + // De-duplicate payload in recent time + if (BLESensorConfiguration.filterDuplicatePayloadData != TimeInterval.never) { + final long removePayloadDataBefore = new Date().getTime() - BLESensorConfiguration.filterDuplicatePayloadData.millis(); + for (Map.Entry entry : didReadPayloadData.entrySet()) { + if (entry.getValue().getTime() < removePayloadDataBefore) { + didReadPayloadData.remove(entry.getKey()); + } + } + final Date lastReportedAt = didReadPayloadData.get(payloadData); + if (lastReportedAt != null) { + logger.debug("didRead, filtered duplicate (device={},payloadData={},lastReportedAt={})", device, device.payloadData().shortName(), lastReportedAt); + return; + } + didReadPayloadData.put(payloadData, new Date()); + } + // Notify delegates + logger.debug("didRead (device={},payloadData={},payloadData={})", device, device.payloadData(), payloadData.shortName()); + final RSSI rssi = device.rssi(); + final Proximity proximity = new Proximity(ProximityMeasurementUnit.RSSI, (double) rssi.value); + operationQueue.execute(new Runnable() { + @Override + public void run() { + for (SensorDelegate delegate : delegates) { + if (device.txPower() != null) { + delegate.sensor(SensorType.BLE, payloadData, device.identifier, proximity, device.txPower().value, device); + } else { + delegate.sensor(SensorType.BLE, payloadData, device.identifier, proximity, 999, device); + } + } + } + }); + break; + } + default: { + } + } + } + + @Override + public void bleDatabaseDidDelete(BLEDevice device) { + logger.debug("didDelete (device={})", device.identifier); + } + + // MARK:- BluetoothStateManagerDelegate + + @Override + public void bluetoothStateManager(BluetoothState didUpdateState) { + logger.debug("didUpdateState (state={})", didUpdateState); + SensorState sensorState = SensorState.off; + if (didUpdateState == BluetoothState.poweredOn) { + sensorState = SensorState.on; + } else if (didUpdateState == BluetoothState.unsupported) { + sensorState = SensorState.unavailable; + } + for (SensorDelegate delegate : delegates) { + delegate.sensor(SensorType.BLE, sensorState); + } + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLETransmitter.kt b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLETransmitter.kt new file mode 100644 index 0000000..f8af50c --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLETransmitter.kt @@ -0,0 +1,610 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// +package au.gov.health.covidsafe.sensor.ble + +import android.bluetooth.* +import android.bluetooth.le.AdvertiseCallback +import android.bluetooth.le.AdvertiseData +import android.bluetooth.le.AdvertiseSettings +import android.bluetooth.le.BluetoothLeAdvertiser +import android.content.Context +import android.os.ParcelUuid +import au.gov.health.covidsafe.sensor.SensorDelegate +import au.gov.health.covidsafe.sensor.data.ConcreteSensorLogger +import au.gov.health.covidsafe.sensor.data.SensorLogger +import au.gov.health.covidsafe.sensor.datatype.* +import au.gov.health.covidsafe.sensor.payload.PayloadDataSupplier +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicReference + + +class ConcreteBLETransmitter( + private val context: Context, + private val bluetoothStateManager: BluetoothStateManager, + timer: BLETimer, + private val payloadDataSupplier: PayloadDataSupplier, + private val database: BLEDatabase) : BLETransmitter, BluetoothStateManagerDelegate { + private val logger: SensorLogger = ConcreteSensorLogger("Sensor", "BLE.ConcreteBLETransmitter") + private val operationQueue = Executors.newSingleThreadExecutor() + + // Referenced by startAdvert and stopExistingGattServer ONLY + private var bluetoothGattServer: BluetoothGattServer? = null + + override fun add(delegate: SensorDelegate) { + BLETransmitter.delegates.add(delegate) + } + + override fun start() { + logger.debug("start (supported={})", isSupported) + // advertLoop is started by Bluetooth state + } + + override fun stop() { + logger.debug("stop") + // advertLoop is stopped by Bluetooth state + } + + // MARK:- Advert loop + private enum class AdvertLoopState { + starting, started, stopping, stopped + } + + /// Get Bluetooth LE advertiser + private fun bluetoothLeAdvertiser(): BluetoothLeAdvertiser? { + val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + if (bluetoothAdapter == null) { + logger.debug("bluetoothLeAdvertiser, no Bluetooth Adapter available") + return null + } + val supported = bluetoothAdapter.isMultipleAdvertisementSupported + return try { + val bluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser + if (bluetoothLeAdvertiser == null) { + logger.debug("bluetoothLeAdvertiser, no LE advertiser present (multiSupported={}, exception=no)", supported) + return null + } + // log this, as this will allow us to identify handsets with a different API implementation + logger.debug("bluetoothLeAdvertiser, LE advertiser present (multiSupported={})", supported) + bluetoothLeAdvertiser + } catch (e: Exception) { + // log it, as this will allow us to identify handsets with the expected API implementation (from Android API source code) + logger.debug("bluetoothLeAdvertiser, no LE advertiser present (multiSupported={}, exception={})", supported, e.message) + null + } + } + + private inner class AdvertLoopTask : BLETimerDelegate { + private var advertLoopState = AdvertLoopState.stopped + private var lastStateChangeAt = System.currentTimeMillis() + private var advertiseCallback: AdvertiseCallback? = null + private fun state(now: Long, state: AdvertLoopState) { + val elapsed = now - lastStateChangeAt + logger.debug("advertLoopTask, state change (from={},to={},elapsed={}ms)", advertLoopState, state, elapsed) + advertLoopState = state + lastStateChangeAt = now + } + + private fun timeSincelastStateChange(now: Long): Long { + return now - lastStateChangeAt + } + + override fun bleTimer(now: Long) { + if (!isSupported || bluetoothStateManager.state() == BluetoothState.poweredOff) { + if (advertLoopState != AdvertLoopState.stopped) { + advertiseCallback = null + bluetoothGattServer = null + state(now, AdvertLoopState.stopped) + logger.debug("advertLoopTask, stop advert (advert={}ms)", timeSincelastStateChange(now)) + } + return + } + when (advertLoopState) { + AdvertLoopState.stopped -> { + if (bluetoothStateManager.state() == BluetoothState.poweredOn) { + val period = timeSincelastStateChange(now) + if (period >= advertOffDurationMillis) { + logger.debug("advertLoopTask, start advert (stop={}ms)", period) + val bluetoothLeAdvertiser = bluetoothLeAdvertiser() + if (bluetoothLeAdvertiser == null) { + logger.fault("advertLoopTask, start advert denied, Bluetooth LE advertiser unavailable") + return + } + state(now, AdvertLoopState.starting) + startAdvert(bluetoothLeAdvertiser, Callback { value -> + advertiseCallback = value.b + bluetoothGattServer = value.c + state(now, if (value.a) AdvertLoopState.started else AdvertLoopState.stopped) + }) + } + } + return + } + AdvertLoopState.started -> { + val period = timeSincelastStateChange(now) + if (period >= BLESensorConfiguration.advertRefreshTimeInterval.millis()) { + logger.debug("advertLoopTask, stop advert (advert={}ms)", period) + val bluetoothLeAdvertiser = bluetoothLeAdvertiser() + if (bluetoothLeAdvertiser == null) { + logger.fault("advertLoopTask, stop advert denied, Bluetooth LE advertiser unavailable") + return + } + state(now, AdvertLoopState.stopping) + stopAdvert(bluetoothLeAdvertiser, advertiseCallback, bluetoothGattServer, object : Callback { + override fun accept(value: Boolean) { + advertiseCallback = null + bluetoothGattServer = null + state(now, AdvertLoopState.stopped) + } + }) + } + return + } + } + } + } + + private fun stopExistingGattServer() { + if (null != bluetoothGattServer) { + // Stop old version, if there's already a proxy reference + bluetoothGattServer = try { + bluetoothGattServer!!.clearServices() + bluetoothGattServer!!.close() + null + } catch (e2: Throwable) { + logger.fault("stopGattServer failed to stop EXISTING GATT server", e2) + null + } + } + } + + // MARK:- Start and stop advert + private fun startAdvert(bluetoothLeAdvertiser: BluetoothLeAdvertiser, callback: Callback>) { + logger.debug("startAdvert") + operationQueue.execute(Runnable { + var result = true + // var bluetoothGattServer: BluetoothGattServer? = null + stopExistingGattServer() + try { + bluetoothGattServer = startGattServer(logger, context, payloadDataSupplier, database) + } catch (e: Throwable) { + logger.fault("startAdvert failed to start GATT server", e) + result = false + } + if (bluetoothGattServer == null) { + result = false + } else { + try { + setGattService(logger, context, bluetoothGattServer) + } catch (e: Throwable) { + if (null != bluetoothGattServer) { + logger.fault("startAdvert failed to set GATT service", e) + bluetoothGattServer = try { + bluetoothGattServer!!.clearServices() + bluetoothGattServer!!.close() + null + } catch (e2: Throwable) { + logger.fault("startAdvert failed to stop GATT server", e2) + null + } + } + result = false + } + } + if (!result) { + logger.fault("startAdvert failed") + callback.accept(Triple(false, null, null)) + return@Runnable + } + try { + val bluetoothGattServerConfirmed = bluetoothGattServer + val advertiseCallback: AdvertiseCallback = object : AdvertiseCallback() { + override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { + logger.debug("startAdvert successful") + callback.accept(Triple(true, this, bluetoothGattServerConfirmed)) + } + + override fun onStartFailure(errorCode: Int) { + logger.fault("startAdvert failed (errorCode={})", onStartFailureErrorCodeToString(errorCode)) + callback.accept(Triple(false, this, bluetoothGattServerConfirmed)) + } + } + startAdvertising(bluetoothLeAdvertiser, advertiseCallback) + } catch (e: Throwable) { + logger.fault("startAdvert failed") + callback.accept(Triple(false, null, null)) + } + }) + } + + private fun stopAdvert(bluetoothLeAdvertiser: BluetoothLeAdvertiser, advertiseCallback: AdvertiseCallback?, bluetoothGattServer: BluetoothGattServer?, callback: Callback) { + logger.debug("stopAdvert") + operationQueue.execute { + var result = true + try { + if (advertiseCallback != null) { + bluetoothLeAdvertiser.stopAdvertising(advertiseCallback) + } + } catch (e: Throwable) { + logger.fault("stopAdvert failed to stop advertising", e) + result = false + } + try { + if (bluetoothGattServer != null) { + bluetoothGattServer.clearServices() + bluetoothGattServer.close() + } + } catch (e: Throwable) { + logger.fault("stopAdvert failed to stop GATT server", e) + result = false + } + if (result) { + logger.debug("stopAdvert successful") + } else { + logger.fault("stopAdvert failed") + } + callback.accept(result) + } + } + + override fun payloadData(): PayloadData { + return payloadDataSupplier.payload(PayloadTimestamp(Date())) + } + + override fun isSupported(): Boolean { + return bluetoothLeAdvertiser() != null + } + + override fun bluetoothStateManager(didUpdateState: BluetoothState) { + logger.debug("didUpdateState (state={})", didUpdateState) + if (didUpdateState == BluetoothState.poweredOn) { + start() + } else if (didUpdateState == BluetoothState.poweredOff) { + stop() + } + } + + private fun startAdvertising(bluetoothLeAdvertiser: BluetoothLeAdvertiser, advertiseCallback: AdvertiseCallback) { + logger.debug("startAdvertising") + val settings = AdvertiseSettings.Builder() + .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED) + .setConnectable(true) + .setTimeout(0) + .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_LOW) + .build() + val pseudoDeviceAddress = PseudoDeviceAddress() + val data = AdvertiseData.Builder() + .setIncludeDeviceName(false) + .setIncludeTxPowerLevel(false) + .addServiceUuid(ParcelUuid(BLESensorConfiguration.serviceUUID)) + .addManufacturerData(BLESensorConfiguration.manufacturerIdForSensor, pseudoDeviceAddress.data) + .build() + bluetoothLeAdvertiser.startAdvertising(settings, data, advertiseCallback) + logger.debug("startAdvertising successful (pDeviceAddress={},settings={})", pseudoDeviceAddress, settings) + } + + companion object { + private val TAG = "ConcreteBLETransmitter" + val gson: Gson = GsonBuilder().disableHtmlEscaping().create() + var bluetoothGattServer: BluetoothGattServer? = null + + private val advertOffDurationMillis = TimeInterval.seconds(4).millis() + private fun startGattServer(logger: SensorLogger, context: Context, payloadDataSupplier: PayloadDataSupplier, database: BLEDatabase): BluetoothGattServer? { + logger.debug("startGattServer") + val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + if (bluetoothManager == null) { + logger.fault("Bluetooth unsupported") + return null + } + // Data = rssi (4 bytes int) + payload (remaining bytes) + val server = AtomicReference(null) + val callback: BluetoothGattServerCallback = object : BluetoothGattServerCallback() { + + //this should be a table + //in order to handle many connections from different mac addresses + //val writeDataPayload: MutableMap = HashMap() + //val readPayloadMap: MutableMap = HashMap() + + ////in order to handle many connections from different mac addresses + //Implement as Same as GattServer in Covid here + private val onCharacteristicReadPayloadData: MutableMap = ConcurrentHashMap() + private val onCharacteristicWriteSignalData: MutableMap = ConcurrentHashMap() + private fun onCharacteristicReadPayloadData(device: BluetoothDevice): PayloadData? { + logger.debug("startGattServer") + //Come here if othe phone is older version + val key = device.address + if (onCharacteristicReadPayloadData.containsKey(key)) { + return onCharacteristicReadPayloadData[key] + } + val payloadData = payloadDataSupplier.payload(PayloadTimestamp()) + onCharacteristicReadPayloadData[key] = payloadData + return payloadData + } + + private fun onCharacteristicWriteSignalData(device: BluetoothDevice, value: ByteArray?): ByteArray { + logger.debug("startGattServer") + val key = device.address + var partialData = onCharacteristicWriteSignalData[key] + if (partialData == null) { + partialData = ByteArray(0) + } + val data = ByteArray(partialData.size + (value?.size ?: 0)) + System.arraycopy(partialData, 0, data, 0, partialData.size) + if (value != null) { + System.arraycopy(value, 0, data, partialData.size, value.size) + } + onCharacteristicWriteSignalData[key] = data + return data + } + + private fun removeData(device: BluetoothDevice) { + val deviceAddress = device.address + for (deviceRequestId in ArrayList(onCharacteristicReadPayloadData.keys)) { + if (deviceRequestId.startsWith(deviceAddress)) { + onCharacteristicReadPayloadData.remove(deviceRequestId) + } + } + for (deviceRequestId in ArrayList(onCharacteristicWriteSignalData.keys)) { + if (deviceRequestId.startsWith(deviceAddress)) { + onCharacteristicWriteSignalData.remove(deviceRequestId) + } + } + } + + override fun onConnectionStateChange(bluetoothDevice: BluetoothDevice?, status: Int, newState: Int) { + val device = database.device(bluetoothDevice) + logger.debug("onConnectionStateChange (device={},status={},newState={})", + device, status, onConnectionStateChangeStatusToString(newState)) + if (newState == BluetoothProfile.STATE_CONNECTED) { + //Save data here + device.state(BLEDeviceState.connected) + if (bluetoothDevice != null) { + bluetoothManager.getConnectedDevices(BluetoothProfile.GATT).contains(bluetoothDevice) + } + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + device.state(BLEDeviceState.disconnected) + bluetoothDevice?.let { removeData(bluetoothDevice) } + } + } + + // TODO We receive payload here + override fun onCharacteristicWriteRequest(device: BluetoothDevice?, requestId: Int, characteristic: BluetoothGattCharacteristic, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray?) { + device?.let { + val targetDevice = database.device(device) + val targetIdentifier = targetDevice.identifier + logger.debug("didReceiveWrite (central={},requestId={},offset={},characteristic={},value={})", + targetDevice, requestId, offset, + if (characteristic.uuid == BLESensorConfiguration.androidSignalCharacteristicUUID) "signal" else "unknown", + value?.size ?: "null" + ) + val data = Data(onCharacteristicWriteSignalData(device, value)) + if (characteristic.uuid == BLESensorConfiguration.legacyCovidsafePayloadCharacteristicUUID) { +// val payloadData = SignalCharacteristicData.decodeWritePayload(data) +// ?: // Fragmented payload data may be incomplete +// return + val payloadData = SignalCharacteristicData.decodeWritePayload(data) + logger.debug("didReceiveWrite (dataType=payload,central={},payload={})", targetDevice, payloadData) + // Only receive-only Android devices write payload +// targetDevice.operatingSystem(BLEDeviceOperatingSystem.android) +// targetDevice.receiveOnly(true) +// targetDevice.payloadData(payloadData) +// onCharacteristicWriteSignalData.remove(device.address) + if (responseNeeded) { + server.get()?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value) + } + return + } + if (characteristic.uuid !== BLESensorConfiguration.androidSignalCharacteristicUUID) { + if (responseNeeded) { + server.get()?.sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, offset, value) + } + return + } + when (SignalCharacteristicData.detect(data)) { + SignalCharacteristicDataType.rssi -> { + val rssi = SignalCharacteristicData.decodeWriteRSSI(data) + if (rssi == null) { + logger.fault("didReceiveWrite, invalid request (central={},action=writeRSSI)", targetDevice) + return + } + logger.debug("didReceiveWrite (dataType=rssi,central={},rssi={})", targetDevice, rssi) + // Only receive-only Android devices write RSSI + targetDevice.operatingSystem(BLEDeviceOperatingSystem.android) + targetDevice.receiveOnly(true) + targetDevice.rssi(rssi) + return + } + SignalCharacteristicDataType.payload -> { + val payloadData = SignalCharacteristicData.decodeWritePayload(data) + ?: // Fragmented payload data may be incomplete + return + logger.debug("didReceiveWrite (dataType=payload,central={},payload={})", targetDevice, payloadData) + // Only receive-only Android devices write payload + targetDevice.operatingSystem(BLEDeviceOperatingSystem.android) + targetDevice.receiveOnly(true) + targetDevice.payloadData(payloadData) + onCharacteristicWriteSignalData.remove(device.address) + if (responseNeeded) { + server.get()?.sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, offset, value) + } + return + } + SignalCharacteristicDataType.payloadSharing -> { + val payloadSharingData = SignalCharacteristicData.decodeWritePayloadSharing(data) + ?: // Fragmented payload sharing data may be incomplete + return + val didSharePayloadData = payloadDataSupplier.payload(payloadSharingData.data) + for (delegate in BLETransmitter.delegates) { + delegate.sensor(SensorType.BLE, didSharePayloadData, targetIdentifier) + } + // Only Android devices write payload sharing + targetDevice.operatingSystem(BLEDeviceOperatingSystem.android) + targetDevice.rssi(payloadSharingData.rssi) + logger.debug("didReceiveWrite (dataType=payloadSharing,central={},payloadSharingData={})", targetDevice, didSharePayloadData) + for (payloadData in didSharePayloadData) { + val sharedDevice = database.device(payloadData) + sharedDevice.operatingSystem(BLEDeviceOperatingSystem.shared) + sharedDevice.rssi(payloadSharingData.rssi) + } + return + } + } + if (responseNeeded) { + server.get()?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value) + } + } + } + + inner class ReadRequestEncryptedPayload(val timestamp: Long, val modelP: String, val msg: String?) + + override fun onCharacteristicReadRequest(device: BluetoothDevice?, requestId: Int, offset: Int, characteristic: BluetoothGattCharacteristic?) { + device?.let { + //Come here if other phone is older version + val targetDevice = database.device(device) + if (characteristic?.uuid === BLESensorConfiguration.payloadCharacteristicUUID || characteristic?.uuid === BLESensorConfiguration.legacyCovidsafePayloadCharacteristicUUID) { + val payloadData = onCharacteristicReadPayloadData(device) + payloadData?.let { + if (offset > payloadData.value.size) { + logger.fault("didReceiveRead, invalid offset (central={},requestId={},offset={},characteristic=payload,dataLength={})", targetDevice, requestId, offset, payloadData.value.size) + server.get()?.sendResponse(device, requestId, BluetoothGatt.GATT_INVALID_OFFSET, offset, null) + } else { + val value = Arrays.copyOfRange(payloadData.value, offset, payloadData.value.size) + server.get()?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value) + logger.debug("didReceiveRead (central={},requestId={},offset={},characteristic=payload)", targetDevice, requestId, offset) + } + } + } +// else if (characteristic?.uuid === BLESensorConfiguration.serviceUUID) { +// //write here +// val peripheral = TracerApp.asPeripheralDevice() +// val readRequest = ReadRequestEncryptedPayload( +// System.currentTimeMillis() / 1000L, +// peripheral.modelP, +// TracerApp.thisDeviceMsg() +// ) +// val plainRecord = gson.toJson(readRequest) +// CentralLog.d(TAG, "onCharacteristicReadRequest plainRecord = $plainRecord") +// +// val plainRecordByteArray = plainRecord.toByteArray(Charsets.UTF_8) +// val remoteBlob = Encryption.encryptPayload(plainRecordByteArray) +// val base = readPayloadMap.getOrPut(device.address, { +// ReadRequestPayload( +// v = TracerApp.protocolVersion, +// msg = remoteBlob, +// org = TracerApp.ORG, +// modelP = null //This is going to be stored as empty in the db as DUMMY value +// ).getPayload() +// }) +// val value = base.copyOfRange(offset, base.size) +// server.get()?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, value) +// +// } + else { + logger.fault("didReceiveRead (central={},characteristic=unknown)", targetDevice) + server.get()?.sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, 0, null) + } + } + } + } + server.set(bluetoothManager.openGattServer(context, callback)) + logger.debug("startGattServer successful") + return server.get() + } + + //Here + private fun setGattService(logger: SensorLogger, context: Context, bluetoothGattServer: BluetoothGattServer?) { + logger.debug("setGattService") + val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + if (bluetoothManager == null) { + logger.fault("Bluetooth unsupported") + return + } + if (bluetoothGattServer == null) { + logger.fault("Bluetooth LE advertiser unsupported") + return + } + for (device in bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)) { + bluetoothGattServer.cancelConnection(device) + } + for (device in bluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER)) { + bluetoothGattServer.cancelConnection(device) + } + bluetoothGattServer.clearServices() + // Logic check - ensure there are now no Gatt Services + var services = bluetoothGattServer.services + for (svc in services) { + logger.fault("setGattService device clearServices() call did not correctly clear service (service={})", svc.uuid) + } + + val service = BluetoothGattService(BLESensorConfiguration.serviceUUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) + val signalCharacteristic = BluetoothGattCharacteristic( + BLESensorConfiguration.androidSignalCharacteristicUUID, + BluetoothGattCharacteristic.PROPERTY_WRITE, + BluetoothGattCharacteristic.PERMISSION_WRITE) + signalCharacteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + val payloadCharacteristic = BluetoothGattCharacteristic( + BLESensorConfiguration.payloadCharacteristicUUID, + BluetoothGattCharacteristic.PROPERTY_READ, + BluetoothGattCharacteristic.PERMISSION_READ) + val legacyPayloadCharacteristicUUID = BluetoothGattCharacteristic( + BLESensorConfiguration.legacyCovidsafePayloadCharacteristicUUID, + BluetoothGattCharacteristic.PROPERTY_READ, + BluetoothGattCharacteristic.PERMISSION_READ) + service.addCharacteristic(signalCharacteristic) + service.addCharacteristic(payloadCharacteristic) + service.addCharacteristic(legacyPayloadCharacteristicUUID) + bluetoothGattServer.addService(service) + + // Logic check - ensure there can be only one Herald service + services = bluetoothGattServer.services + var count = 0 + for (svc in services) { + if (svc.uuid == BLESensorConfiguration.serviceUUID) { + count++ + } + } + if (count > 1) { + logger.fault("setGattService device incorrectly sharing multiple Herald services (count={})", count) + } + + logger.debug("setGattService successful (service={},signalCharacteristic={},payloadCharacteristic={})", + service.uuid, signalCharacteristic.uuid, payloadCharacteristic.uuid) + } + + private fun onConnectionStateChangeStatusToString(state: Int): String { + + + return when (state) { + BluetoothProfile.STATE_CONNECTED -> "STATE_CONNECTED" + BluetoothProfile.STATE_CONNECTING -> "STATE_CONNECTING" + BluetoothProfile.STATE_DISCONNECTING -> "STATE_DISCONNECTING" + BluetoothProfile.STATE_DISCONNECTED -> "STATE_DISCONNECTED" + else -> "UNKNOWN_STATE_$state" + } + } + + private fun onStartFailureErrorCodeToString(errorCode: Int): String { + return when (errorCode) { + AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE -> "ADVERTISE_FAILED_DATA_TOO_LARGE" + AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS" + AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED -> "ADVERTISE_FAILED_ALREADY_STARTED" + AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR -> "ADVERTISE_FAILED_INTERNAL_ERROR" + AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> "ADVERTISE_FAILED_FEATURE_UNSUPPORTED" + else -> "UNKNOWN_ERROR_CODE_$errorCode" + } + } + } + + /** + * Transmitter starts automatically when Bluetooth is enabled. + */ + init { + BluetoothStateManager.delegates.add(this) + bluetoothStateManager(bluetoothStateManager.state()) + timer.add(AdvertLoopTask()) + } +} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBluetoothStateManager.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBluetoothStateManager.java new file mode 100644 index 0000000..2968df8 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBluetoothStateManager.java @@ -0,0 +1,86 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble; + +import android.bluetooth.BluetoothAdapter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import au.gov.health.covidsafe.sensor.data.ConcreteSensorLogger; +import au.gov.health.covidsafe.sensor.data.SensorLogger; +import au.gov.health.covidsafe.sensor.datatype.BluetoothState; + +/** + * Monitors bluetooth state changes. + */ +public class ConcreteBluetoothStateManager implements BluetoothStateManager { + private final SensorLogger logger = new ConcreteSensorLogger("Sensor", "BLE.ConcreteBluetoothStateManager"); + private BluetoothState state; + private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) { + try { + final int nativeState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); + logger.debug("Bluetooth state changed (nativeState={})", nativeState); + switch (nativeState) { + case BluetoothAdapter.STATE_ON: + logger.debug("Power ON"); + state = BluetoothState.poweredOn; + for (BluetoothStateManagerDelegate delegate : delegates) { + delegate.bluetoothStateManager(BluetoothState.poweredOn); + } + break; + case BluetoothAdapter.STATE_OFF: + logger.debug("Power OFF"); + state = BluetoothState.poweredOff; + for (BluetoothStateManagerDelegate delegate : delegates) { + delegate.bluetoothStateManager(BluetoothState.poweredOff); + } + break; + } + } catch (Throwable e) { + logger.fault("Bluetooth state change exception", e); + } + } + } + }; + + /** + * Monitors bluetooth state changes. + */ + public ConcreteBluetoothStateManager(Context context) { + state = state(); + + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); + context.registerReceiver(broadcastReceiver, intentFilter); + } + + @Override + public BluetoothState state() { + if (state == null) { + final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (bluetoothAdapter == null) { + state = BluetoothState.unsupported; + return state; + } + switch (BluetoothAdapter.getDefaultAdapter().getState()) { + case BluetoothAdapter.STATE_ON: + state = BluetoothState.poweredOn; + break; + case BluetoothAdapter.STATE_OFF: + case BluetoothAdapter.STATE_TURNING_OFF: + case BluetoothAdapter.STATE_TURNING_ON: + default: + state = BluetoothState.poweredOff; + break; + } + } + return state; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertAppleManufacturerSegment.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertAppleManufacturerSegment.java new file mode 100644 index 0000000..a8eaa2c --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertAppleManufacturerSegment.java @@ -0,0 +1,26 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble.filter; + +import au.gov.health.covidsafe.sensor.datatype.Data; + +public class BLEAdvertAppleManufacturerSegment { + public final int type; + public final int reportedLength; + public final byte[] data; // BIG ENDIAN (network order) AT THIS POINT + public final Data raw; + + public BLEAdvertAppleManufacturerSegment(int type, int reportedLength, byte[] dataBigEndian, Data raw) { + this.type = type; + this.reportedLength = reportedLength; + this.data = dataBigEndian; + this.raw = raw; + } + + @Override + public String toString() { + return raw.hexEncodedString(); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertManufacturerData.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertManufacturerData.java new file mode 100644 index 0000000..ceb8ce7 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertManufacturerData.java @@ -0,0 +1,28 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble.filter; + +import au.gov.health.covidsafe.sensor.datatype.Data; + +public class BLEAdvertManufacturerData { + public final int manufacturer; + public final byte[] data; // BIG ENDIAN (network order) AT THIS POINT + public final Data raw; + + public BLEAdvertManufacturerData(final int manufacturer, final byte[] dataBigEndian, final Data raw) { + this.manufacturer = manufacturer; + this.data = dataBigEndian; + this.raw = raw; + } + + @Override + public String toString() { + return "BLEAdvertManufacturerData{" + + "manufacturer=" + manufacturer + + ", data=" + new Data(data).hexEncodedString() + + ", raw=" + raw.hexEncodedString() + + '}'; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertParser.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertParser.java new file mode 100644 index 0000000..46c651f --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertParser.java @@ -0,0 +1,162 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble.filter; + +import au.gov.health.covidsafe.sensor.datatype.Data; +import au.gov.health.covidsafe.sensor.datatype.UInt8; + +import java.util.ArrayList; +import java.util.List; + +public class BLEAdvertParser { + public static BLEScanResponseData parseScanResponse(byte[] raw, int offset) { + // Multiple segments until end of binary data + return new BLEScanResponseData(raw.length - offset, extractSegments(raw, offset)); + } + + public static List extractSegments(byte[] raw, int offset) { + int position = offset; + ArrayList segments = new ArrayList(); + int segmentLength; + int segmentType; + byte[] segmentData; + Data rawData; + int c; + + while (position < raw.length) { + if ((position + 2) <= raw.length) { + segmentLength = (byte)raw[position++] & 0xff; + segmentType = (byte)raw[position++] & 0xff; + // Note: Unsupported types are handled as 'unknown' + // check reported length with actual remaining data length + if ((position + segmentLength - 1) <= raw.length) { + segmentData = subDataBigEndian(raw, position, segmentLength - 1); // Note: type IS INCLUDED in length + rawData = new Data(subDataBigEndian(raw, position - 2, segmentLength + 1)); + position += segmentLength - 1; + segments.add(new BLEAdvertSegment(BLEAdvertSegmentType.typeFor(segmentType), segmentLength - 1, segmentData, rawData)); + } else { + // error in data length - advance to end + position = raw.length; + } + } else { + // invalid segment - advance to end + position = raw.length; + } + } + + return segments; + } + + public static String hex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02x", b)); + } + return result.toString(); + } + + public static String binaryString(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0')); + result.append(" "); + } + return result.toString(); + } + + public static byte[] subDataBigEndian(byte[] raw, int offset, int length) { + if (raw == null) { + return new byte[]{}; + } + if (offset < 0 || length <= 0) { + return new byte[]{}; + } + if (length + offset > raw.length) { + return new byte[]{}; + } + byte[] data = new byte[length]; + int position = offset; + for (int c = 0;c < length;c++) { + data[c] = raw[position++]; + } + return data; + } + + public static byte[] subDataLittleEndian(byte[] raw, int offset, int length) { + if (raw == null) { + return new byte[]{}; + } + if (offset < 0 || length <= 0) { + return new byte[]{}; + } + if (length + offset > raw.length) { + return new byte[]{}; + } + byte[] data = new byte[length]; + int position = offset + length - 1; + for (int c = 0;c < length;c++) { + data[c] = raw[position--]; + } + return data; + } + + public static Integer extractTxPower(List segments) { + // find the txPower code segment in the list + for (BLEAdvertSegment segment : segments) { + if (segment.type == BLEAdvertSegmentType.txPowerLevel) { + return (new UInt8((int)segment.data[0])).value; + } + } + return null; + } + + public static List extractManufacturerData(List segments) { + // find the manufacturerData code segment in the list + List manufacturerData = new ArrayList<>(); + for (BLEAdvertSegment segment : segments) { + if (segment.type == BLEAdvertSegmentType.manufacturerData) { + // Ensure that the data area is long enough + if (segment.data.length < 2) { + continue; // there may be a valid segment of same type... Happens for manufacturer data + } + // Create a manufacturer data segment + int intValue = ((segment.data[1]&0xff) << 8) | (segment.data[0]&0xff); + manufacturerData.add(new BLEAdvertManufacturerData(intValue,subDataBigEndian(segment.data,2,segment.dataLength - 2), segment.raw)); + } + } + return manufacturerData; + } + + public static List extractAppleManufacturerSegments(List manuData) { + final List appleSegments = new ArrayList<>(); + for (BLEAdvertManufacturerData manu : manuData) { + int bytePos = 0; + while (bytePos < manu.data.length) { + final byte type = manu.data[bytePos]; + final int typeValue = type & 0xFF; + // "01" marks legacy service UUID encoding without length data + if (type == 0x01) { + final int length = manu.data.length - bytePos - 1; + final Data data = new Data(subDataBigEndian(manu.data, bytePos + 1, length)); + final Data raw = new Data(subDataBigEndian(manu.data, bytePos, manu.data.length - bytePos)); + final BLEAdvertAppleManufacturerSegment segment = new BLEAdvertAppleManufacturerSegment(typeValue, length, data.value, raw); + appleSegments.add(segment); + bytePos = manu.data.length; + } + // Parse according to Type-Length-Data + else { + final int length = manu.data[bytePos + 1] & 0xFF; + final int maxLength = (length < manu.data.length - bytePos - 2 ? length : manu.data.length - bytePos - 2); + final Data data = new Data(subDataBigEndian(manu.data, bytePos + 2, maxLength)); + final Data raw = new Data(subDataBigEndian(manu.data, bytePos, maxLength + 2)); + final BLEAdvertAppleManufacturerSegment segment = new BLEAdvertAppleManufacturerSegment(typeValue, length, data.value, raw); + appleSegments.add(segment); + bytePos += (maxLength + 2); + } + } + } + return appleSegments; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertSegment.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertSegment.java new file mode 100644 index 0000000..fbb5401 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertSegment.java @@ -0,0 +1,31 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble.filter; + +import au.gov.health.covidsafe.sensor.datatype.Data; + +public class BLEAdvertSegment { + public final BLEAdvertSegmentType type; + public final int dataLength; + public final byte[] data; // BIG ENDIAN (network order) AT THIS POINT + public final Data raw; + + public BLEAdvertSegment(BLEAdvertSegmentType type, int dataLength, byte[] data, Data raw) { + this.type = type; + this.dataLength = dataLength; + this.data = data; + this.raw = raw; + } + + @Override + public String toString() { + return "BLEAdvertSegment{" + + "type=" + type + + ", dataLength=" + dataLength + + ", data=" + new Data(data).hexEncodedString() + + ", raw=" + raw.hexEncodedString() + + '}'; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertSegmentType.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertSegmentType.java new file mode 100644 index 0000000..4bc1e4f --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEAdvertSegmentType.java @@ -0,0 +1,66 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble.filter; + +import java.util.HashMap; +import java.util.Map; + +/// BLE Advert types - Note: We only list those we use in Herald for some reason +/// See https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/ +public enum BLEAdvertSegmentType { + unknown("unknown", 0x00), // Valid - this number is not assigned + serviceUUID16IncompleteList("serviceUUID16IncompleteList", 0x02), + serviceUUID16CompleteList("serviceUUID16CompleteList", 0x03), + serviceUUID32IncompleteList("serviceUUID32IncompleteList", 0x04), + serviceUUID32CompleteList("serviceUUID32CompleteList", 0x05), + serviceUUID128IncompleteList("serviceUUID128IncompleteList", 0x06), + serviceUUID128CompleteList("serviceUUID128CompleteList", 0x07), + deviceNameShortened("deviceNameShortened", 0x08), + deviceNameComplete("deviceNameComplete", 0x09), + txPowerLevel("txPower",0x0A), + deviceClass("deviceClass",0x0D), + simplePairingHash("simplePairingHash",0x0E), + simplePairingRandomiser("simplePairingRandomiser",0x0F), + deviceID("deviceID",0x10), + meshMessage("meshMessage",0x2A), + meshBeacon("meshBeacon",0x2B), + bigInfo("bigInfo",0x2C), + broadcastCode("broadcastCode",0x2D), + manufacturerData("manufacturerData", 0xFF) + ; + + private static final Map BY_LABEL = new HashMap<>(); + private static final Map BY_CODE = new HashMap<>(); + static { + for (BLEAdvertSegmentType e : values()) { + BY_LABEL.put(e.label, e); + BY_CODE.put(e.code, e); + } + } + + public final String label; + public final int code; + + private BLEAdvertSegmentType(String label, int code) { + this.label = label; + this.code = code; + } + + public static BLEAdvertSegmentType typeFor(int code) { + BLEAdvertSegmentType type = BY_CODE.get(code); + if (null == type) { + return BY_LABEL.get("unknown"); + } + return type; + } + + public static BLEAdvertSegmentType typeFor(String commonName) { + BLEAdvertSegmentType type = BY_LABEL.get(commonName); + if (null == type) { + return BY_LABEL.get("unknown"); + } + return type; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEDeviceFilter.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEDeviceFilter.java new file mode 100644 index 0000000..907f306 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEDeviceFilter.java @@ -0,0 +1,196 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble.filter; + +import android.bluetooth.le.ScanRecord; + +import au.gov.health.covidsafe.sensor.ble.BLEDevice; +import au.gov.health.covidsafe.sensor.ble.BLESensorConfiguration; +import au.gov.health.covidsafe.sensor.data.ConcreteSensorLogger; +import au.gov.health.covidsafe.sensor.data.SensorLogger; +import au.gov.health.covidsafe.sensor.datatype.Data; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/// Device filter for avoiding connection to devices that definitely cannot +/// host sensor services. +public class BLEDeviceFilter { + private final static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private final SensorLogger logger = new ConcreteSensorLogger("Sensor", "BLE.BLEDeviceFilter"); + private final List filterPatterns; + private final Map samples = new HashMap<>(); + + // Counter for training samples + private final static class ShouldIgnore { + public long yes = 0; + public long no = 0; + } + + // Pattern for filtering device based on message content + public final static class FilterPattern { + public final String regularExpression; + public final Pattern pattern; + public FilterPattern(final String regularExpression, final Pattern pattern) { + this.regularExpression = regularExpression; + this.pattern = pattern; + } + } + + // Match of a filter pattern + public final static class MatchingPattern { + public final FilterPattern filterPattern; + public final String message; + public MatchingPattern(FilterPattern filterPattern, String message) { + this.filterPattern = filterPattern; + this.message = message; + } + } + + /// BLE device filter for matching devices against filters defined + /// in BLESensorConfiguration.deviceFilterFeaturePatterns. + public BLEDeviceFilter() { + this(BLESensorConfiguration.deviceFilterFeaturePatterns); + } + + /// BLE device filter for matching devices against the given set of patterns + /// and writing advert data to file for analysis. + public BLEDeviceFilter(final String[] patterns) { + if (patterns == null || patterns.length == 0) { + filterPatterns = null; + } else { + filterPatterns = compilePatterns(patterns); + } + } + + // MARK:- Pattern matching functions + // Using regular expression over hex representation of feature data for maximum flexibility and usability + + /// Match message against all patterns in sequential order, returns matching pattern or null + protected static FilterPattern match(final List filterPatterns, final String message) { + final SensorLogger logger = new ConcreteSensorLogger("Sensor", "BLE.BLEDeviceFilter"); + if (message == null) { + return null; + } + for (final FilterPattern filterPattern : filterPatterns) { + try { + final Matcher matcher = filterPattern.pattern.matcher(message); + if (matcher.find()) { + return filterPattern; + } + } catch (Throwable e) { + } + } + return null; + } + + /// Compile regular expressions into patterns. + protected static List compilePatterns(final String[] regularExpressions) { + final SensorLogger logger = new ConcreteSensorLogger("Sensor", "BLE.BLEDeviceFilter"); + final List filterPatterns = new ArrayList<>(regularExpressions.length); + for (final String regularExpression : regularExpressions) { + try { + final Pattern pattern = Pattern.compile(regularExpression, Pattern.CASE_INSENSITIVE); + final FilterPattern filterPattern = new FilterPattern(regularExpression, pattern); + filterPatterns.add(filterPattern); + } catch (Throwable e) { + logger.fault("compilePatterns, invalid filter pattern (regularExpression={})", regularExpression); + } + } + return filterPatterns; + } + + /// Extract messages from manufacturer specific data + protected final static List extractMessages(final byte[] rawScanRecordData) { + // Parse raw scan record data in scan response data + if (rawScanRecordData == null || rawScanRecordData.length == 0) { + return null; + } + final BLEScanResponseData bleScanResponseData = BLEAdvertParser.parseScanResponse(rawScanRecordData, 0); + // Parse scan response data into manufacturer specific data + if (bleScanResponseData == null || bleScanResponseData.segments == null || bleScanResponseData.segments.isEmpty()) { + return null; + } + final List bleAdvertManufacturerDataList = BLEAdvertParser.extractManufacturerData(bleScanResponseData.segments); + // Parse manufacturer specific data into messages + if (bleAdvertManufacturerDataList == null || bleAdvertManufacturerDataList.isEmpty()) { + return null; + } + final List bleAdvertAppleManufacturerSegments = BLEAdvertParser.extractAppleManufacturerSegments(bleAdvertManufacturerDataList); + // Convert segments to messages + if (bleAdvertAppleManufacturerSegments == null || bleAdvertAppleManufacturerSegments.isEmpty()) { + return null; + } + final List messages = new ArrayList<>(bleAdvertAppleManufacturerSegments.size()); + for (BLEAdvertAppleManufacturerSegment segment : bleAdvertAppleManufacturerSegments) { + if (segment.raw != null && segment.raw.value.length > 0) { + messages.add(segment.raw); + } + } + return messages; + } + + // MARK:- Filtering functions + + /// Extract feature data from scan record + private List extractFeatures(final ScanRecord scanRecord) { + if (scanRecord == null) { + return null; + } + // Get message data + final List featureList = new ArrayList<>(); + final List messages = extractMessages(scanRecord.getBytes()); + if (messages != null) { + featureList.addAll(messages); + } + return featureList; + } + + /// Match filter patterns against data items, returning the first match + protected final static MatchingPattern match(final List patternList, final Data rawData) { + // No pattern to match against + if (patternList == null || patternList.isEmpty()) { + return null; + } + // Empty raw data + if (rawData == null || rawData.value.length == 0) { + return null; + } + // Extract messages + final List messages = extractMessages(rawData.value); + if (messages == null || messages.isEmpty()) { + return null; + } + for (Data message : messages) { + final String hexEncodedString = message.hexEncodedString(); + final FilterPattern pattern = match(patternList, hexEncodedString); + if (pattern != null) { + return new MatchingPattern(pattern, hexEncodedString); + } + } + return null; + } + + /// Match scan record messages against all registered patterns, returns matching pattern or null. + public MatchingPattern match(final BLEDevice device) { + try { + final ScanRecord scanRecord = device.scanRecord(); + // Cannot match device without any scan record data + if (scanRecord == null) { + return null; + } + final Data rawData = new Data(scanRecord.getBytes()); + return match(filterPatterns, rawData); + } catch (Throwable e) { + // Errors are expected to be common place due to corrupted or malformed advert data + return null; + } + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEScanResponseData.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEScanResponseData.java new file mode 100644 index 0000000..f266607 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/filter/BLEScanResponseData.java @@ -0,0 +1,22 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.ble.filter; + +import java.util.List; + +public class BLEScanResponseData { + public int dataLength; + public List segments; + + public BLEScanResponseData(int dataLength, List segments) { + this.dataLength = dataLength; + this.segments = segments; + } + + @Override + public String toString() { + return segments.toString(); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/data/BatteryLog.java b/app/src/main/java/au/gov/health/covidsafe/sensor/data/BatteryLog.java new file mode 100644 index 0000000..677068a --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/data/BatteryLog.java @@ -0,0 +1,67 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.data; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; + +import au.gov.health.covidsafe.sensor.datatype.TimeInterval; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/// CSV battery log for post event analysis and visualisation +public class BatteryLog { + private final SensorLogger logger = new ConcreteSensorLogger("Sensor", "BatteryLog"); + private final static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private final static TimeInterval updateInterval = TimeInterval.seconds(30); + private final Context context; + private final TextFile textFile; + + public BatteryLog(final Context context, final String filename) { + this.context = context; + textFile = new TextFile(context, filename); + if (textFile.empty()) { + textFile.write("time,source,level"); + } + new Thread(new Runnable() { + @Override + public void run() { + while (true) { + try { + update(); + } catch (Throwable e) { + logger.fault("Update failed", e); + } + try { + Thread.sleep(updateInterval.millis()); + } catch (Throwable e) { + logger.fault("Timer interrupted", e); + } + } + } + }).start(); + } + + private void update() { + final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + final Intent batteryStatus = context.registerReceiver(null, intentFilter); + if (batteryStatus == null) { + return; + } + final int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + final boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL; + final int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + final int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + final float batteryLevel = level * 100 / (float) scale; + + final String powerSource = (isCharging ? "external" : "battery"); + final String timestamp = dateFormatter.format(new Date()); + textFile.write(timestamp + "," + powerSource + "," + batteryLevel); + logger.debug("update (powerSource={},batteryLevel={})", powerSource, batteryLevel); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/data/ConcreteSensorLogger.java b/app/src/main/java/au/gov/health/covidsafe/sensor/data/ConcreteSensorLogger.java new file mode 100644 index 0000000..b81bdd7 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/data/ConcreteSensorLogger.java @@ -0,0 +1,148 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.data; + +import android.content.Context; +import android.util.Log; + +import au.gov.health.covidsafe.BuildConfig; +import au.gov.health.covidsafe.sensor.ble.BLESensorConfiguration; + +import java.text.SimpleDateFormat; +import java.util.Date; + +public class ConcreteSensorLogger implements SensorLogger { + private final String subsystem, category; + private final static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private static Context context; + private static TextFile logFile; + + public ConcreteSensorLogger(String subsystem, String category) { + this.subsystem = subsystem; + this.category = category; + } + + public static void context(final Context context) { + if (context != null && context != ConcreteSensorLogger.context) { + ConcreteSensorLogger.context = context; + if (BuildConfig.DEBUG) { + logFile = new TextFile(context, "log.txt"); + } + } + } + + private boolean suppress(SensorLoggerLevel level) { + switch (level) { + case debug: + return (BLESensorConfiguration.logLevel == SensorLoggerLevel.info || BLESensorConfiguration.logLevel == SensorLoggerLevel.fault); + case info: + return (BLESensorConfiguration.logLevel == SensorLoggerLevel.fault); + default: + return false; + } + } + + private void log(SensorLoggerLevel level, String message, final Object... values) { + if (!suppress(level)) { + outputLog(level, tag(subsystem, category), message, values); + outputStream(level, subsystem, category, message, values); + } + } + + public void debug(String message, final Object... values) { + log(SensorLoggerLevel.debug, message, values); + } + + public void info(String message, final Object... values) { + log(SensorLoggerLevel.info, message, values); + } + + public void fault(String message, final Object... values) { + log(SensorLoggerLevel.fault, message, values); + } + + private static String tag(String subsystem, String category) { + return subsystem + "::" + category; + } + + private static void outputLog(final SensorLoggerLevel level, final String tag, final String message, final Object... values) { + final Throwable throwable = getThrowable(values); + switch (level) { + case debug: { + if (throwable == null) { + Log.d(tag, render(message, values)); + } else { + Log.d(tag, render(message, values), throwable); + } + break; + } + case info: { + if (throwable == null) { + Log.i(tag, render(message, values)); + } else { + Log.i(tag, render(message, values), throwable); + } + break; + } + case fault: { + if (throwable == null) { + Log.w(tag, render(message, values)); + } else { + Log.w(tag, render(message, values), throwable); + } + break; + } + } + } + + private static void outputStream(final SensorLoggerLevel level, final String subsystem, final String category, final String message, final Object... values) { + if (logFile == null) { + return; + } + final String timestamp = dateFormatter.format(new Date()); + final String csvMessage = render(message, values).replace('\"', '\''); + final String quotedMessage = (message.contains(",") ? "\"" + csvMessage + "\"" : csvMessage); + final String entry = timestamp + "," + level + "," + subsystem + "," + category + "," + quotedMessage; + logFile.write(entry); + } + + + private static Throwable getThrowable(final Object... values) { + if (values.length > 0 && values[values.length - 1] instanceof Throwable) { + return (Throwable) values[values.length - 1]; + } else { + return null; + } + } + + private static String render(final String message, final Object... values) { + if (values.length == 0) { + return message; + } else { + final StringBuilder stringBuilder = new StringBuilder(); + + int valueIndex = 0; + int start = 0; + int end = message.indexOf("{}"); + while (end > 0) { + stringBuilder.append(message.substring(start, end)); + if (values.length > valueIndex) { + if (values[valueIndex] == null) { + stringBuilder.append("NULL"); + } else { + stringBuilder.append(values[valueIndex].toString()); + } + } + valueIndex++; + start = end + 2; + end = message.indexOf("{}", start); + } + stringBuilder.append(message.substring(start)); + + return stringBuilder.toString(); + } + } + +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/data/ContactLog.java b/app/src/main/java/au/gov/health/covidsafe/sensor/data/ContactLog.java new file mode 100644 index 0000000..d3e9ed7 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/data/ContactLog.java @@ -0,0 +1,69 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.data; + +import android.content.Context; + +import au.gov.health.covidsafe.sensor.DefaultSensorDelegate; +import au.gov.health.covidsafe.sensor.datatype.Location; +import au.gov.health.covidsafe.sensor.datatype.PayloadData; +import au.gov.health.covidsafe.sensor.datatype.Proximity; +import au.gov.health.covidsafe.sensor.datatype.SensorType; +import au.gov.health.covidsafe.sensor.datatype.TargetIdentifier; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +/// CSV contact log for post event analysis and visualisation +public class ContactLog extends DefaultSensorDelegate { + private final static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private final TextFile textFile; + + public ContactLog(final Context context, final String filename) { + textFile = new TextFile(context, filename); + if (textFile.empty()) { + textFile.write("time,sensor,id,detect,read,measure,share,visit,data"); + } + } + + private String timestamp() { + return dateFormatter.format(new Date()); + } + + private String csv(String value) { + return TextFile.csv(value); + } + + // MARK:- SensorDelegate + + @Override + public void sensor(SensorType sensor, TargetIdentifier didDetect) { + textFile.write(timestamp() + "," + sensor.name() + "," + csv(didDetect.value) + ",1,,,,,"); + } + + @Override + public void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget) { + textFile.write(timestamp() + "," + sensor.name() + "," + csv(fromTarget.value) + ",,2,,,," + csv(didRead.shortName())); + } + + @Override + public void sensor(SensorType sensor, List didShare, TargetIdentifier fromTarget) { + final String prefix = timestamp() + "," + sensor.name() + "," + csv(fromTarget.value); + for (PayloadData payloadData : didShare) { + textFile.write(prefix + ",,,,4,," + csv(payloadData.shortName())); + } + } + + @Override + public void sensor(SensorType sensor, Proximity didMeasure, TargetIdentifier fromTarget) { + textFile.write(timestamp() + "," + sensor.name() + "," + csv(fromTarget.value) + ",,,3,,," + csv(didMeasure.description())); + } + + @Override + public void sensor(SensorType sensor, Location didVisit) { + textFile.write(timestamp() + "," + sensor.name() + ",,,,,,5," + csv(didVisit.description())); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/data/DetectionLog.java b/app/src/main/java/au/gov/health/covidsafe/sensor/data/DetectionLog.java new file mode 100644 index 0000000..e531e2e --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/data/DetectionLog.java @@ -0,0 +1,87 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.data; + +import android.content.Context; + +import au.gov.health.covidsafe.sensor.DefaultSensorDelegate; +import au.gov.health.covidsafe.sensor.ble.BLEDevice; +import au.gov.health.covidsafe.sensor.datatype.PayloadData; +import au.gov.health.covidsafe.sensor.datatype.Proximity; +import au.gov.health.covidsafe.sensor.datatype.SensorType; +import au.gov.health.covidsafe.sensor.datatype.TargetIdentifier; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/// CSV contact log for post event analysis and visualisation +public class DetectionLog extends DefaultSensorDelegate { + private final SensorLogger logger = new ConcreteSensorLogger("Sensor", "Data.DetectionLog"); + private final TextFile textFile; + private final PayloadData payloadData; + private final String deviceName = android.os.Build.MODEL; + private final String deviceOS = Integer.toString(android.os.Build.VERSION.SDK_INT); + private final Map payloads = new ConcurrentHashMap<>(); + + public DetectionLog(final Context context, final String filename, final PayloadData payloadData) { + textFile = new TextFile(context, filename); + this.payloadData = payloadData; + write(); + } + + private String csv(String value) { + return TextFile.csv(value); + } + + private void write() { + final StringBuilder content = new StringBuilder(); + content.append(csv(deviceName)); + content.append(','); + content.append("Android"); + content.append(','); + content.append(csv(deviceOS)); + content.append(','); + content.append(csv(payloadData.shortName())); + final List payloadList = new ArrayList<>(payloads.size()); + for (String payload : payloads.keySet()) { + if (payload.equals(payloadData.shortName())) { + continue; + } + payloadList.add(payload); + } + Collections.sort(payloadList); + for (String payload : payloadList) { + content.append(','); + content.append(payload); + } + logger.debug("write (content={})", content.toString()); + content.append("\n"); + textFile.overwrite(content.toString()); + } + + + // MARK:- SensorDelegate + + @Override + public void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget) { + if (payloads.put(didRead.shortName(), fromTarget.value) == null) { + logger.debug("didRead (payload={})", payloadData.shortName()); + write(); + } + } + + @Override + public void sensor(SensorType sensor, List didShare, TargetIdentifier fromTarget) { + for (PayloadData payloadData : didShare) { + if (payloads.put(payloadData.shortName(), fromTarget.value) == null) { + logger.debug("didShare (payload={})", payloadData.shortName()); + write(); + } + } + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/data/SensorLogger.java b/app/src/main/java/au/gov/health/covidsafe/sensor/data/SensorLogger.java new file mode 100644 index 0000000..bd519fc --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/data/SensorLogger.java @@ -0,0 +1,14 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.data; + +public interface SensorLogger { + + void debug(String message, final Object... values); + + void info(String message, final Object... values); + + void fault(String message, final Object... values); +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/data/SensorLoggerLevel.java b/app/src/main/java/au/gov/health/covidsafe/sensor/data/SensorLoggerLevel.java new file mode 100644 index 0000000..a043277 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/data/SensorLoggerLevel.java @@ -0,0 +1,9 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.data; + +public enum SensorLoggerLevel { + debug, info, fault +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/data/StatisticsLog.java b/app/src/main/java/au/gov/health/covidsafe/sensor/data/StatisticsLog.java new file mode 100644 index 0000000..52e763f --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/data/StatisticsLog.java @@ -0,0 +1,116 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.data; + +import android.content.Context; + +import au.gov.health.covidsafe.sensor.DefaultSensorDelegate; +import au.gov.health.covidsafe.sensor.datatype.PayloadData; +import au.gov.health.covidsafe.sensor.datatype.Proximity; +import au.gov.health.covidsafe.sensor.datatype.Sample; +import au.gov.health.covidsafe.sensor.datatype.SensorType; +import au.gov.health.covidsafe.sensor.datatype.TargetIdentifier; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/// CSV contact log for post event analysis and visualisation +public class StatisticsLog extends DefaultSensorDelegate { + private final TextFile textFile; + private final PayloadData payloadData; + private final Map identifierToPayload = new ConcurrentHashMap<>(); + private final Map payloadToTime = new ConcurrentHashMap<>(); + private final Map payloadToSample = new ConcurrentHashMap<>(); + + public StatisticsLog(final Context context, final String filename, final PayloadData payloadData) { + textFile = new TextFile(context, filename); + this.payloadData = payloadData; + } + + private String csv(String value) { + return TextFile.csv(value); + } + + private void add(TargetIdentifier identifier) { + final String payload = identifierToPayload.get(identifier); + if (payload == null) { + return; + } + add(payload); + } + + private void add(String payload) { + final Date time = payloadToTime.get(payload); + final Sample sample = payloadToSample.get(payload); + if (time == null || sample == null) { + payloadToTime.put(payload, new Date()); + payloadToSample.put(payload, new Sample()); + return; + } + final Date now = new Date(); + payloadToTime.put(payload, now); + sample.add((now.getTime() - time.getTime()) / 1000d); + write(); + } + + private void write() { + final StringBuilder content = new StringBuilder("payload,count,mean,sd,min,max\n"); + final List payloadList = new ArrayList<>(); + for (String payload : payloadToSample.keySet()) { + if (payload.equals(payloadData.shortName())) { + continue; + } + payloadList.add(payload); + } + Collections.sort(payloadList); + for (String payload : payloadList) { + final Sample sample = payloadToSample.get(payload); + if (sample == null) { + continue; + } + if (sample.mean() == null || sample.standardDeviation() == null || sample.min() == null || sample.max() == null) { + continue; + } + content.append(csv(payload)); + content.append(','); + content.append(sample.count()); + content.append(','); + content.append(sample.mean()); + content.append(','); + content.append(sample.standardDeviation()); + content.append(','); + content.append(sample.min()); + content.append(','); + content.append(sample.max()); + content.append('\n'); + } + textFile.overwrite(content.toString()); + } + + + // MARK:- SensorDelegate + + @Override + public void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget) { + identifierToPayload.put(fromTarget, didRead.shortName()); + add(fromTarget); + } + + @Override + public void sensor(SensorType sensor, Proximity didMeasure, TargetIdentifier fromTarget) { + add(fromTarget); + } + + @Override + public void sensor(SensorType sensor, List didShare, TargetIdentifier fromTarget) { + for (PayloadData payload : didShare) { + add(payload.shortName()); + } + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/data/TextFile.java b/app/src/main/java/au/gov/health/covidsafe/sensor/data/TextFile.java new file mode 100644 index 0000000..07688e2 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/data/TextFile.java @@ -0,0 +1,91 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.data; + +import android.content.Context; +import android.media.MediaScannerConnection; +import android.os.Environment; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class TextFile { + private final SensorLogger logger = new ConcreteSensorLogger("Sensor", "Data.TextFile"); + private final File file; + + public TextFile(final Context context, final String filename) { + final File folder = new File(getRootFolder(context), "Sensor"); + if (!folder.exists()) { + if (!folder.mkdirs()) { + logger.fault("Make folder failed (folder={})", folder); + } + } + file = new File(folder, filename); + final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); + executorService.scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null, null); + } + }, 30, 30, TimeUnit.SECONDS); + } + + /** + * Get root folder for SD card or emulated external storage. + * + * @param context Application context. + * @return Root folder. + */ + private static File getRootFolder(final Context context) { + // Get SD card or emulated external storage. By convention (really!?) + // SD card is reported after emulated storage, so select the last folder + final File[] externalMediaDirs = context.getExternalMediaDirs(); + if (externalMediaDirs.length > 0) { + return externalMediaDirs[externalMediaDirs.length - 1]; + } else { + return Environment.getExternalStorageDirectory(); + } + } + + public synchronized boolean empty() { + return !file.exists() || file.length() == 0; + } + + /// Append line to new or existing file + public synchronized void write(String line) { + try { + final FileOutputStream fileOutputStream = new FileOutputStream(file, true); + fileOutputStream.write((line + "\n").getBytes()); + fileOutputStream.flush(); + fileOutputStream.close(); + } catch (Throwable e) { + logger.fault("write failed (file={})", file, e); + } + } + + /// Overwrite file content + public synchronized void overwrite(String content) { + try { + final FileOutputStream fileOutputStream = new FileOutputStream(file); + fileOutputStream.write(content.getBytes()); + fileOutputStream.flush(); + fileOutputStream.close(); + } catch (Throwable e) { + logger.fault("overwrite failed (file={})", file, e); + } + } + + /// Quote value for CSV output if required. + public static String csv(String value) { + if (value.contains(",") || value.contains("\"") || value.contains("'") || value.contains("’")) { + return "\"" + value + "\""; + } else { + return value; + } + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Base64.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Base64.java new file mode 100644 index 0000000..293d1f1 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Base64.java @@ -0,0 +1,96 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +import java.io.ByteArrayOutputStream; + +/// Base64 encoding and decoding without relying on Android API 26+ +public class Base64 { + private final static char[] encodeTable = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; + + private final static int[] decodeTable = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, + 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + + public static String encode(byte[] data) { + final StringBuilder buffer = new StringBuilder(); + int pad = 0; + for (int i = 0; i < data.length; i += 3) { + int b = ((data[i] & 0xFF) << 16) & 0xFFFFFF; + if (i + 1 < data.length) { + b |= (data[i + 1] & 0xFF) << 8; + } else { + pad++; + } + if (i + 2 < data.length) { + b |= (data[i + 2] & 0xFF); + } else { + pad++; + } + for (int j = 0; j < 4 - pad; j++) { + int c = (b & 0xFC0000) >> 18; + buffer.append(encodeTable[c]); + b <<= 6; + } + } + for (int j = 0; j < pad; j++) { + buffer.append("="); + } + return buffer.toString(); + } + + public static byte[] decode(String data) { + final byte[] bytes = data.getBytes(); + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + for (int i = 0; i < bytes.length; ) { + int b = 0; + if (decodeTable[bytes[i]] != -1) { + b = (decodeTable[bytes[i]] & 0xFF) << 18; + } else { + // skip unknown characters + i++; + continue; + } + int num = 0; + if (i + 1 < bytes.length && decodeTable[bytes[i + 1]] != -1) { + b = b | ((decodeTable[bytes[i + 1]] & 0xFF) << 12); + num++; + } + if (i + 2 < bytes.length && decodeTable[bytes[i + 2]] != -1) { + b = b | ((decodeTable[bytes[i + 2]] & 0xFF) << 6); + num++; + } + if (i + 3 < bytes.length && decodeTable[bytes[i + 3]] != -1) { + b = b | (decodeTable[bytes[i + 3]] & 0xFF); + num++; + } + while (num > 0) { + int c = (b & 0xFF0000) >> 16; + buffer.write((char) c); + b <<= 8; + num--; + } + i += 4; + } + return buffer.toByteArray(); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/BluetoothState.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/BluetoothState.java new file mode 100644 index 0000000..52a2856 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/BluetoothState.java @@ -0,0 +1,9 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +public enum BluetoothState { + unsupported, poweredOn, poweredOff, resetting +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Callback.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Callback.java new file mode 100644 index 0000000..b896749 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Callback.java @@ -0,0 +1,10 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +/// Generic callback function +public interface Callback { + void accept(T value); +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Data.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Data.java new file mode 100644 index 0000000..d2d7f8a --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Data.java @@ -0,0 +1,117 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +import java.util.Arrays; + +/// Raw byte array data +public class Data { + private final static char[] hexChars = "0123456789ABCDEF".toCharArray(); + public byte[] value = null; + + public Data() { + this(new byte[0]); + } + + public Data(byte[] value) { + this.value = value; + } + + public Data(final Data data) { + final byte[] value = new byte[data.value.length]; + System.arraycopy(data.value, 0, value, 0, data.value.length); + this.value = value; + } + + public Data(byte repeating, int count) { + this.value = new byte[count]; + for (int i=count; i-->0;) { + this.value[i] = repeating; + } + } + + public Data(String base64EncodedString) { + this.value = Base64.decode(base64EncodedString); + } + + public String base64EncodedString() { + return Base64.encode(value); + } + + public String hexEncodedString() { + if (value == null) { + return ""; + } + final StringBuilder stringBuilder = new StringBuilder(value.length * 2); + for (int i = 0; i < value.length; i++) { + final int v = value[i] & 0xFF; + stringBuilder.append(hexChars[v >>> 4]); + stringBuilder.append(hexChars[v & 0x0F]); + } + return stringBuilder.toString(); + } + + public final static Data fromHexEncodedString(String hexEncodedString) { + final int length = hexEncodedString.length(); + final byte[] value = new byte[length / 2]; + for (int i = 0; i < length; i += 2) { + value[i / 2] = (byte) ((Character.digit(hexEncodedString.charAt(i), 16) << 4) + + Character.digit(hexEncodedString.charAt(i+1), 16)); + } + return new Data(value); + } + + public String description() { + return base64EncodedString(); + } + + /// Get subdata from offset to end + public Data subdata(int offset) { + if (offset < value.length) { + final byte[] offsetValue = new byte[value.length - offset]; + System.arraycopy(value, offset, offsetValue, 0, offsetValue.length); + return new Data(offsetValue); + } else { + return null; + } + } + + /// Get subdata from offset to offset + length + public Data subdata(int offset, int length) { + if (offset + length <= value.length) { + final byte[] offsetValue = new byte[length]; + System.arraycopy(value, offset, offsetValue, 0, length); + return new Data(offsetValue); + } else { + return null; + } + } + + /// Append data to end of this data. + public void append(Data data) { + final byte[] concatenated = new byte[value.length + data.value.length]; + System.arraycopy(value, 0, concatenated, 0, value.length); + System.arraycopy(data.value, 0, concatenated, value.length, data.value.length); + value = concatenated; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Data data = (Data) o; + return Arrays.equals(value, data.value); + } + + @Override + public int hashCode() { + return Arrays.hashCode(value); + } + + @Override + public String toString() { + return hexEncodedString(); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Location.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Location.java new file mode 100644 index 0000000..90eed20 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Location.java @@ -0,0 +1,26 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +import java.util.Date; + +/// Raw location data for estimating indirect exposure +public class Location { + /// Measurement values, e.g. GPS coordinates in comma separated string format for latitude and longitude + public final LocationReference value; + /// Time spent at location. + public final Date start, end; + + public Location(LocationReference value, Date start, Date end) { + this.value = value; + this.start = start; + this.end = end; + } + + /// Get plain text description of proximity data + public String description() { + return value.description() + ":[from=" + start + ",to=" + end + "]"; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/LocationReference.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/LocationReference.java new file mode 100644 index 0000000..2b1526d --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/LocationReference.java @@ -0,0 +1,10 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +/// Raw location data for estimating indirect exposure, e.g. WGS84 coordinates +public interface LocationReference { + String description(); +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PayloadData.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PayloadData.java new file mode 100644 index 0000000..86c8764 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PayloadData.java @@ -0,0 +1,41 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +/// Encrypted payload data received from target. This is likely to be an encrypted datagram of the target's actual permanent identifier. +public class PayloadData extends Data { + public PayloadData(byte[] value) { + super(value); + } + + public PayloadData(String base64EncodedString) { + super(base64EncodedString); + } + + public PayloadData(byte repeating, int count) { + super(repeating, count); + } + + public PayloadData() { + this(new byte[0]); + } + + public String shortName() { + if (value.length == 0) { + return ""; + } + if (!(value.length > 3)) { + return Base64.encode(value); + } + final Data subdata = subdata(3, value.length - 3); + final byte[] suffix = (subdata == null || subdata.value == null ? new byte[0] : subdata.value); + final String base64EncodedString = Base64.encode(suffix); + return base64EncodedString.substring(0, Math.min(6, base64EncodedString.length())); + } + + public String toString() { + return shortName(); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PayloadSharingData.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PayloadSharingData.java new file mode 100644 index 0000000..2bedc8b --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PayloadSharingData.java @@ -0,0 +1,21 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +public class PayloadSharingData { + public final RSSI rssi; + public final Data data; + + /** + * Payload sharing data + * + * @param rssi RSSI between self and peer. + * @param data Payload data of devices being shared by self to peer. + */ + public PayloadSharingData(final RSSI rssi, final Data data) { + this.rssi = rssi; + this.data = data; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PayloadTimestamp.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PayloadTimestamp.java new file mode 100644 index 0000000..436cf58 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PayloadTimestamp.java @@ -0,0 +1,20 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +import java.util.Date; + +/// Payload timestamp, should normally be Date, but it may change to UInt64 in the future to use server synchronised relative timestamp. +public class PayloadTimestamp { + public final Date value; + + public PayloadTimestamp(Date value) { + this.value = value; + } + + public PayloadTimestamp() { + this(new Date()); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PlacenameLocationReference.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PlacenameLocationReference.java new file mode 100644 index 0000000..84ed5aa --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PlacenameLocationReference.java @@ -0,0 +1,18 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +/// Free text place name. +public class PlacenameLocationReference implements LocationReference { + public final String name; + + public PlacenameLocationReference(String name) { + this.name = name; + } + + public String description() { + return "PLACE(name=" + name + ")"; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Proximity.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Proximity.java new file mode 100644 index 0000000..7d44782 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Proximity.java @@ -0,0 +1,31 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +import androidx.annotation.NonNull; + +/// Raw data for estimating proximity between sensor and target, e.g. RSSI for BLE. +public class Proximity { + /// Unit of measurement, e.g. RSSI + public final ProximityMeasurementUnit unit; + /// Measured value, e.g. raw RSSI value. + public final Double value; + + public Proximity(ProximityMeasurementUnit unit, Double value) { + this.unit = unit; + this.value = value; + } + + /// Get plain text description of proximity data + public String description() { + return unit + ":" + value; + } + + @NonNull + @Override + public String toString() { + return description(); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/ProximityMeasurementUnit.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/ProximityMeasurementUnit.java new file mode 100644 index 0000000..290508a --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/ProximityMeasurementUnit.java @@ -0,0 +1,13 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +/// Measurement unit for interpreting the proximity data values. +public enum ProximityMeasurementUnit { + /// Received signal strength indicator, e.g. BLE signal strength as proximity estimator. + RSSI, + /// Roundtrip time, e.g. Audio signal echo time duration as proximity estimator. + RTT +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PseudoDeviceAddress.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PseudoDeviceAddress.java new file mode 100644 index 0000000..3ddb053 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PseudoDeviceAddress.java @@ -0,0 +1,148 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +import android.util.Base64; + +import au.gov.health.covidsafe.sensor.data.ConcreteSensorLogger; +import au.gov.health.covidsafe.sensor.data.SensorLogger; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.SecureRandom; +import java.util.Objects; +import java.util.Random; + +/// Pseudo device address to enable caching of device payload without relying on device mac address +// that may change frequently like the A10 and A20. +public class PseudoDeviceAddress { + public final long address; + public final byte[] data; + + /// Using secure random can cause blocking on app initialisation due to lack of entropy + /// on some devices. Worst case scenario is app blocking upon initialisation, bluetooth + /// power cycle, or advert refresh that occurs once every 15 minutes, leading to zero + /// detection until sufficient entropy has been collected, which may take time given + /// the device is likely to be idle. Not using secure random is acceptable and recommended + /// in this instance because it is non-blocking and the sequence has sufficient uncertainty + /// introduced programmatically to make an attack impractical from limited obeservations. + public PseudoDeviceAddress() { + // Bluetooth device address is 48-bit (6 bytes), using + // the same length to offer the same collision avoidance + // Choose between random, secure random, and NIST compliant secure random as random source + // - Random is non-blocking and sufficiently secure for this purpose, recommended + // - SecureRandom is potentially blocking and unnecessary in this instance, not recommended + // - NISTSecureRandom is most likely to block and unnecessary in this instance, not recommended + this.data = encode(getSecureRandomLong()); + this.address = decode(this.data); + } + + public PseudoDeviceAddress(final byte[] data) { + this.data = data; + this.address = decode(data); + } + + /// Non-blocking random number generator with appropriate strength for this purpose + protected final static long getRandomLong() { + // Use a different instance with random seed from another sequence each time + final Random random = new Random(Math.round(Math.random() * Long.MAX_VALUE)); + // Skip a random number of bytes from another sequence + random.nextBytes(new byte[256 + (int) Math.round(Math.random() * 1024)]); + return random.nextLong(); + } + + /// Secure random number generator that is potentially blocking. Experiments have + /// shown blocking can occur, especially on idle device, due to lack of entropy. + protected final static long getSecureRandomLong() { + return new SecureRandom().nextLong(); + } + + private static SecureRandom secureRandomSingleton = null; + /// Secure random number generator that is potentially blocking. + protected final static long getSecureRandomSingletonLong() { + // On-demand initialisation in the hope that sufficient + // entropy has been gathered during app initialisation + if (secureRandomSingleton == null) { + secureRandomSingleton = new SecureRandom(); + } + return secureRandomSingleton.nextLong(); + } + + /// Get secure random instance seed according to NIST SP800-90A recommendations + /// - SHA1PRNG algorithm + /// - Algorithm seeded with 440 bits of secure random data + /// - Skips first random number of bytes to mitigate against poor implementations + /// Compliance to NIST SP800-90A offers quality assurance against an accepted + /// standard. The aim here is not to offer the most perfect random source, but + /// a source with well defined and understood characteristics, thus enabling + /// selection of the most appropropriate method, given the intented purpose. + /// This implementation supports security strength for NIST SP800-57 + /// Part 1 Revision 5 (informally, generation of cryptographic keys for + /// encryption of sensitive data). + public final static long getNISTSecureRandomLong() { + try { + // Obtain SHA1PRNG specifically where possible for NIST SP800-90A compliance. + // Ignoring Android recommendation to use "new SecureRandom()" because that + // decision was taken based on a single peer reviewed statistical test that + // showed SHA1PRNG has bias. The test has not been adopted by NIST yet which + // already uses 15 other statistical tests for quality assurance. This does + // not mean the new test is invalid, but it is more appropriate for this work + // to adopt and comply with an accepted standard for security assurance. + final SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG"); + // Obtain the most secure PRNG claimed by the platform for generating the seed + // according to Android recommendation. + final SecureRandom secureRandomForSeed = new SecureRandom(); + // NIST SP800-90A (see section 10.1) recommends 440 bit seed for SHA1PRNG + // to support security strength defined in NIST SP800-57 Part 1 Revision 5. + final byte[] seed = secureRandomForSeed.generateSeed(55); + // Seed secure random with 440 bit seed according to NIST SP800-90A recommendation. + secureRandom.setSeed(seed); // seed with random number + // Skip the first 256 - 1280 bytes as mitigation against poor implementations + // of SecureRandom where the initial values are predictable given the seed + secureRandom.nextBytes(new byte[256 + secureRandom.nextInt(1024)]); + return secureRandom.nextLong(); + } catch (Throwable e) { + // Android OS may mandate the use of "new SecureRandom()" and forbid the use + // of a specific provider in the future. Fallback to Android mandated option + // and log the fact that it is no longer NIST SP800-90A compliant. + final SensorLogger logger = new ConcreteSensorLogger("Sensor", "Datatype.PseudoDeviceAddress"); + logger.fault("NIST SP800-90A compliant SecureRandom initialisation failed, reverting back to SecureRandom", e); + return getSecureRandomLong(); + } + } + + protected final static byte[] encode(final long value) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(8); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.putLong(0, value); + final byte[] data = new byte[6]; + System.arraycopy(byteBuffer.array(), 0, data, 0, data.length); + return data; + } + protected final static long decode(final byte[] data) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(8); + byteBuffer.put(data); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + return byteBuffer.getLong(0); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PseudoDeviceAddress that = (PseudoDeviceAddress) o; + return address == that.address; + } + + @Override + public int hashCode() { + return Objects.hash(address); + } + + @Override + public String toString() { + return Base64.encodeToString(data, Base64.DEFAULT | Base64.NO_WRAP); + } +} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/RSSI.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/RSSI.java new file mode 100644 index 0000000..2156b6f --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/RSSI.java @@ -0,0 +1,34 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +/// RSSI in dBm. +public class RSSI { + public final int value; + + public RSSI(int value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RSSI rssi = (RSSI) o; + return value == rssi.value; + } + + @Override + public int hashCode() { + return value; + } + + @Override + public String toString() { + return "RSSI{" + + "value=" + value + + '}'; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Sample.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Sample.java new file mode 100644 index 0000000..67ec750 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Sample.java @@ -0,0 +1,74 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +public class Sample { + protected long n; + protected double m1, m2, m3, m4; + private double min = Double.MAX_VALUE, max = -Double.MAX_VALUE; + + public Sample() { + } + + public synchronized void add(final double x) { + final long n1 = n; + n++; + final double delta = x - m1; + final double delta_n = delta / n; + final double delta_n2 = delta_n * delta_n; + final double term1 = delta * delta_n * n1; + m1 += delta_n; + m4 += term1 * delta_n2 * (n * n - 3 * n + 3) + 6 * delta_n2 * m2 - 4 * delta_n * m3; + m3 += term1 * delta_n * (n - 2) - 3 * delta_n * m2; + m2 += term1; + if (x < min) { + min = x; + } + if (x > max) { + max = x; + } + } + + public long count() { + return n; + } + + public Double mean() { + if (n > 0) { + return m1; + } else { + return null; + } + } + + public Double standardDeviation() { + if (n > 1) { + return StrictMath.sqrt(m2 / (n - 1d)); + } else { + return null; + } + } + + public Double min() { + if (n > 0) { + return min; + } else { + return null; + } + } + + public Double max() { + if (n > 0) { + return max; + } else { + return null; + } + } + + @Override + public String toString() { + return "[count=" + count() + ",mean=" + mean() + ",sd=" + standardDeviation() + ",min=" + min() + ",max=" + max() + "]"; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SensorState.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SensorState.java new file mode 100644 index 0000000..c24f474 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SensorState.java @@ -0,0 +1,15 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +/// Sensor state +public enum SensorState { + /// Sensor is powered on, active and operational + on, + /// Sensor is powered off, inactive and not operational + off, + /// Sensor is not available + unavailable +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SensorType.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SensorType.java new file mode 100644 index 0000000..2ec7184 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SensorType.java @@ -0,0 +1,17 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +/// Sensor type as qualifier for target identifier. +public enum SensorType { + /// Bluetooth Low Energy (BLE) + BLE, + /// GPS location sensor + GPS +// /// Physical beacon, e.g. iBeacon +// BEACON, +// /// Ultrasound audio beacon. +// ULTRASOUND +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SignalCharacteristicData.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SignalCharacteristicData.java new file mode 100644 index 0000000..7d4d31a --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SignalCharacteristicData.java @@ -0,0 +1,152 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +import au.gov.health.covidsafe.sensor.ble.BLESensorConfiguration; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/// Codec for signal characteristic data bundles +public class SignalCharacteristicData { + + /// Encode write RSSI data bundle + // writeRSSI data format + // 0-0 : actionCode + // 1-2 : rssi value (Int16) + public static Data encodeWriteRssi(final RSSI rssi) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(3); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.put(0, BLESensorConfiguration.signalCharacteristicActionWriteRSSI); + byteBuffer.putShort(1, (short) rssi.value); + return new Data(byteBuffer.array()); + } + + /// Decode write RSSI data bundle + public static RSSI decodeWriteRSSI(final Data data) { + if (signalDataActionCode(data.value) != BLESensorConfiguration.signalCharacteristicActionWriteRSSI) { + return null; + } + if (data.value.length != 3) { + return null; + } + final Short rssiValue = int16(data.value, 1); + if (rssiValue == null) { + return null; + } + return new RSSI(rssiValue.intValue()); + } + + /// Encode write payload data bundle + // writePayload data format + // 0-0 : actionCode + // 1-2 : payload data count in bytes (Int16) + // 3.. : payload data + public static Data encodeWritePayload(final PayloadData payloadData) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(3 + payloadData.value.length); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.put(0, BLESensorConfiguration.signalCharacteristicActionWritePayload); + byteBuffer.putShort(1, (short) payloadData.value.length); + byteBuffer.position(3); + byteBuffer.put(payloadData.value); + return new Data(byteBuffer.array()); + } + + /// Decode write payload data bundle + public static PayloadData decodeWritePayload(final Data data) { + if (signalDataActionCode(data.value) != BLESensorConfiguration.signalCharacteristicActionWritePayload) { + return null; + } + if (data.value.length < 3) { + return null; + } + final Short payloadDataCount = int16(data.value, 1); + if (payloadDataCount == null) { + return null; + } + if (data.value.length != (3 + payloadDataCount.intValue())) { + return null; + } + final Data payloadDataBytes = new Data(data.value).subdata(3); + if (payloadDataBytes == null) { + return null; + } + return new PayloadData(payloadDataBytes.value); + } + + /// Encode write payload sharing data bundle + // writePayloadSharing data format + // 0-0 : actionCode + // 1-2 : rssi value (Int16) + // 3-4 : payload sharing data count in bytes (Int16) + // 5.. : payload sharing data + public static Data encodeWritePayloadSharing(final PayloadSharingData payloadSharingData) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(5 + payloadSharingData.data.value.length); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.put(0, BLESensorConfiguration.signalCharacteristicActionWritePayloadSharing); + byteBuffer.putShort(1, (short) payloadSharingData.rssi.value); + byteBuffer.putShort(3, (short) payloadSharingData.data.value.length); + byteBuffer.position(5); + byteBuffer.put(payloadSharingData.data.value); + return new Data(byteBuffer.array()); + } + + /// Decode write payload data bundle + public static PayloadSharingData decodeWritePayloadSharing(final Data data) { + if (signalDataActionCode(data.value) != BLESensorConfiguration.signalCharacteristicActionWritePayloadSharing) { + return null; + } + if (data.value.length < 5) { + return null; + } + final Short rssiValue = int16(data.value, 1); + if (rssiValue == null) { + return null; + } + final Short payloadSharingDataCount = int16(data.value, 3); + if (payloadSharingDataCount == null) { + return null; + } + if (data.value.length != (5 + payloadSharingDataCount.intValue())) { + return null; + } + final Data payloadSharingDataBytes = new Data(data.value).subdata(5); + if (payloadSharingDataBytes == null) { + return null; + } + return new PayloadSharingData(new RSSI(rssiValue.intValue()), payloadSharingDataBytes); + } + + /// Detect signal characteristic data bundle type + public static SignalCharacteristicDataType detect(Data data) { + switch (signalDataActionCode(data.value)) { + case BLESensorConfiguration.signalCharacteristicActionWriteRSSI: + return SignalCharacteristicDataType.rssi; + case BLESensorConfiguration.signalCharacteristicActionWritePayload: + return SignalCharacteristicDataType.payload; + case BLESensorConfiguration.signalCharacteristicActionWritePayloadSharing: + return SignalCharacteristicDataType.payloadSharing; + default: + return SignalCharacteristicDataType.unknown; + } + } + + private static byte signalDataActionCode(byte[] signalData) { + if (signalData == null || signalData.length == 0) { + return 0; + } + return signalData[0]; + } + + private static Short int16(byte[] data, int index) { + if (index < data.length - 1) { + final ByteBuffer byteBuffer = ByteBuffer.wrap(data); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + return byteBuffer.getShort(index); + } else { + return null; + } + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SignalCharacteristicDataType.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SignalCharacteristicDataType.java new file mode 100644 index 0000000..cffc606 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/SignalCharacteristicDataType.java @@ -0,0 +1,9 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +public enum SignalCharacteristicDataType { + rssi, payload, payloadSharing, unknown +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/TargetIdentifier.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/TargetIdentifier.java new file mode 100644 index 0000000..16ddfaf --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/TargetIdentifier.java @@ -0,0 +1,44 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +import android.bluetooth.BluetoothDevice; + +import java.util.Objects; +import java.util.UUID; + +/// Ephemeral identifier for detected target (e.g. smartphone, beacon, place). +// This is likely to be an UUID but using String for variable identifier length. +public class TargetIdentifier { + public final String value; + + /// Create target identifier based on bluetooth device address + public TargetIdentifier(BluetoothDevice bluetoothDevice) { + this.value = bluetoothDevice.getAddress(); + } + + /// Create random target identifier + public TargetIdentifier() { + this.value = UUID.randomUUID().toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TargetIdentifier that = (TargetIdentifier) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return value; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/TimeInterval.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/TimeInterval.java new file mode 100644 index 0000000..d52046d --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/TimeInterval.java @@ -0,0 +1,37 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +/// Time interval in seconds. +public class TimeInterval { + public final long value; + public static final TimeInterval minute = new TimeInterval(60); + public static final TimeInterval zero = new TimeInterval(0); + public static final TimeInterval never = new TimeInterval(Long.MAX_VALUE); + + public TimeInterval(long seconds) { + this.value = seconds; + } + + public static TimeInterval minutes(long minutes) { + return new TimeInterval(minute.value * minutes); + } + + public static TimeInterval seconds(long seconds) { + return new TimeInterval(seconds); + } + + public long millis() { + return value * 1000; + } + + @Override + public String toString() { + if (value == never.value) { + return "never"; + } + return Long.toString(value); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Triple.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Triple.java new file mode 100644 index 0000000..d034980 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Triple.java @@ -0,0 +1,26 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +public class Triple { + public final A a; + public final B b; + public final C c; + + public Triple(A a, B b, C c) { + this.a = a; + this.b = b; + this.c = c; + } + + @Override + public String toString() { + return "Triple{" + + "a=" + a + + ", b=" + b + + ", c=" + c + + '}'; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Tuple.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Tuple.java new file mode 100644 index 0000000..8f5c375 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Tuple.java @@ -0,0 +1,27 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +public class Tuple { + private final String labelA, labelB; + public final A a; + public final B b; + + public Tuple(A a, B b) { + this("a", a, "b", b); + } + + public Tuple(String labelA, A a, String labelB, B b) { + this.labelA = labelA; + this.labelB = labelB; + this.a = a; + this.b = b; + } + + @Override + public String toString() { + return "(" + labelA + "=" + a + "," + labelB + "=" + b + ")"; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/UInt8.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/UInt8.java new file mode 100644 index 0000000..bc013b7 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/UInt8.java @@ -0,0 +1,23 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/// Unsigned integer (8 bits) +public class UInt8 { + public final int value; + public final Data bigEndian; + + public UInt8(int value) { + assert(value >= 0); + this.value = value; + final ByteBuffer byteBuffer = ByteBuffer.allocate(1); + byteBuffer.order(ByteOrder.BIG_ENDIAN); + byteBuffer.put((byte) value); + this.bigEndian = new Data(byteBuffer.array()); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/WGS84CircularAreaLocationReference.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/WGS84CircularAreaLocationReference.java new file mode 100644 index 0000000..00d0c3d --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/WGS84CircularAreaLocationReference.java @@ -0,0 +1,21 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +/// GPS coordinates and region radius, e.g. latitude and longitude in decimal format and radius in meters. +public class WGS84CircularAreaLocationReference implements LocationReference { + public final Double latitude, longitude, altitude, radius; + + public WGS84CircularAreaLocationReference(Double latitude, Double longitude, Double altitude, Double radius) { + this.latitude = latitude; + this.longitude = longitude; + this.altitude = altitude; + this.radius = radius; + } + + public String description() { + return "WGS84(lat=" + latitude + ",lon=" + longitude + ",alt=" + altitude + ",radius=" + radius + ")"; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/WGS84PointLocationReference.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/WGS84PointLocationReference.java new file mode 100644 index 0000000..5e63cc3 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/WGS84PointLocationReference.java @@ -0,0 +1,20 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.datatype; + +/// GPS coordinates (latitude,longitude,altitude) in WGS84 decimal format and meters from sea level. +public class WGS84PointLocationReference implements LocationReference { + public final Double latitude, longitude, altitude; + + public WGS84PointLocationReference(Double latitude, Double longitude, Double altitude) { + this.latitude = latitude; + this.longitude = longitude; + this.altitude = altitude; + } + + public String description() { + return "WGS84(lat=" + latitude + ",lon=" + longitude + ",alt=" + altitude + ")"; + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/payload/DefaultPayloadDataSupplier.java b/app/src/main/java/au/gov/health/covidsafe/sensor/payload/DefaultPayloadDataSupplier.java new file mode 100644 index 0000000..38c9b8c --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/payload/DefaultPayloadDataSupplier.java @@ -0,0 +1,33 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.payload; + +import au.gov.health.covidsafe.sensor.datatype.Data; +import au.gov.health.covidsafe.sensor.datatype.PayloadData; +import au.gov.health.covidsafe.sensor.datatype.PayloadTimestamp; + +import java.util.ArrayList; +import java.util.List; + +/// Default payload data supplier implementing fixed length payload splitting method. +public abstract class DefaultPayloadDataSupplier implements PayloadDataSupplier { + + @Override + public List payload(Data data) { + // Get fixed length payload data + final PayloadData fixedLengthPayloadData = payload(new PayloadTimestamp()); + final int payloadDataLength = fixedLengthPayloadData.value.length; + // Split raw data comprising of concatenated payloads into individual payloads + final List payloads = new ArrayList<>(); + final byte[] bytes = data.value; + for (int index = 0; (index + payloadDataLength) <= bytes.length; index += payloadDataLength) { + final byte[] payloadBytes = new byte[payloadDataLength]; + System.arraycopy(bytes, index, payloadBytes, 0, payloadDataLength); + payloads.add(new PayloadData(payloadBytes)); + } + return payloads; + } + +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/payload/PayloadDataSupplier.java b/app/src/main/java/au/gov/health/covidsafe/sensor/payload/PayloadDataSupplier.java new file mode 100644 index 0000000..d523c55 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/payload/PayloadDataSupplier.java @@ -0,0 +1,20 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: MIT +// + +package au.gov.health.covidsafe.sensor.payload; + +import au.gov.health.covidsafe.sensor.datatype.Data; +import au.gov.health.covidsafe.sensor.datatype.PayloadData; +import au.gov.health.covidsafe.sensor.datatype.PayloadTimestamp; + +import java.util.List; + +/// Payload data supplier, e.g. BeaconCodes in C19X and BroadcastPayloadSupplier in Sonar. +public interface PayloadDataSupplier { + /// Get payload for given timestamp. Use this for integration with any payload generator, e.g. BeaconCodes or SonarBroadcastPayloadService + PayloadData payload(PayloadTimestamp timestamp); + + /// Parse raw data into payloads + List payload(Data data); +} diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/service/ForegroundService.java b/app/src/main/java/au/gov/health/covidsafe/sensor/service/ForegroundService.java new file mode 100644 index 0000000..572c6b6 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/service/ForegroundService.java @@ -0,0 +1,46 @@ +package au.gov.health.covidsafe.sensor.service; + +import android.app.Notification; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +import au.gov.health.covidsafe.sensor.data.ConcreteSensorLogger; +import au.gov.health.covidsafe.sensor.data.SensorLogger; +import au.gov.health.covidsafe.sensor.datatype.Tuple; + + +/// Foreground service for enabling continuous BLE operation in background +public class ForegroundService extends Service { + private final SensorLogger logger = new ConcreteSensorLogger("App", "ForegroundService"); + + @Override + public void onCreate() { + logger.debug("onCreate"); + super.onCreate(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + logger.debug("onStartCommand"); + +// final NotificationService notificationService = NotificationService.shared(getApplication()); +// final Tuple notification = notificationService.notification("Contact Tracing", "Sensor is working"); +// if (notification.a != null && notification.b != null) { +// startForeground(notification.a, notification.b); +// super.onStartCommand(intent, flags, startId); +// } + return START_STICKY; + } + + @Override + public void onDestroy() { + logger.debug("onDestroy"); + super.onDestroy(); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/service/NotificationService.java b/app/src/main/java/au/gov/health/covidsafe/sensor/service/NotificationService.java new file mode 100644 index 0000000..e7aa875 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/service/NotificationService.java @@ -0,0 +1,81 @@ +package au.gov.health.covidsafe.sensor.service; + +import android.app.Application; +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.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import au.gov.health.covidsafe.R; +import au.gov.health.covidsafe.sensor.datatype.Triple; +import au.gov.health.covidsafe.sensor.datatype.Tuple; + +/// Notification service for enabling foreground service (notification must be displayed to show app is running in the background). +public class NotificationService { + private static NotificationService shared; + private static Application application; + private final Context context; + private final static String notificationChannelName = "NotificationChannel"; + private final int notificationChannelId = notificationChannelName.hashCode(); + private Triple notificationContent = new Triple<>(null, null, null); + + private NotificationService(final Application application) { + this.application = application; + this.context = application.getApplicationContext(); + createNotificationChannel(); + } + + /// Get shared global instance of notification service + public final static NotificationService shared(final Application application) { + if (shared == null) { + shared = new NotificationService(application); + } + return shared; + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + final int importance = NotificationManager.IMPORTANCE_DEFAULT; + final NotificationChannel channel = new NotificationChannel(notificationChannelName, notificationChannelName, importance); + channel.setDescription(notificationChannelName); + final NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } + + public Tuple notification(final String title, final String body) { + if (title != null && body != null) { + final String existingTitle = notificationContent.a; + final String existingBody = notificationContent.b; + if (!title.equals(existingTitle) || !body.equals(existingBody)) { + createNotificationChannel(); + final Intent intent = new Intent(context, application.getClass()); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notificationChannelName) + .setSmallIcon(R.drawable.ic_notification_icon) + .setContentTitle(title) + .setContentText(body) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT); + final Notification notification = builder.build(); + final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.notify(notificationChannelId, notification); + notificationContent = new Triple<>(title, body, notification); + return new Tuple<>(notificationChannelId, notification); + } + } else { + final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.deleteNotificationChannel(notificationChannelName); + notificationContent = new Triple<>(null, null, null); + } + return new Tuple<>(notificationChannelId, null); + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/services/BluetoothMonitoringService.kt b/app/src/main/java/au/gov/health/covidsafe/services/BluetoothMonitoringService.kt index ab78fba..983a413 100644 --- a/app/src/main/java/au/gov/health/covidsafe/services/BluetoothMonitoringService.kt +++ b/app/src/main/java/au/gov/health/covidsafe/services/BluetoothMonitoringService.kt @@ -14,71 +14,48 @@ import android.os.PowerManager import androidx.annotation.Keep import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.LifecycleService -import androidx.localbroadcastmanager.content.LocalBroadcastManager import au.gov.health.covidsafe.BuildConfig import au.gov.health.covidsafe.R import au.gov.health.covidsafe.app.TracerApp -import au.gov.health.covidsafe.bluetooth.BLEAdvertiser -import au.gov.health.covidsafe.bluetooth.gatt.ACTION_RECEIVED_STATUS -import au.gov.health.covidsafe.bluetooth.gatt.ACTION_RECEIVED_STREETPASS -import au.gov.health.covidsafe.bluetooth.gatt.STATUS -import au.gov.health.covidsafe.bluetooth.gatt.STREET_PASS +import au.gov.health.covidsafe.bluetooth.gatt.ReadRequestPayload import au.gov.health.covidsafe.extensions.isLocationEnabledOnDevice import au.gov.health.covidsafe.factory.NetworkFactory import au.gov.health.covidsafe.interactor.usecase.UpdateBroadcastMessageAndPerformScanWithExponentialBackOff import au.gov.health.covidsafe.logging.CentralLog import au.gov.health.covidsafe.notifications.NotificationTemplates import au.gov.health.covidsafe.preference.Preference -import au.gov.health.covidsafe.receivers.PrivacyCleanerReceiver -import au.gov.health.covidsafe.status.Status -import au.gov.health.covidsafe.status.persistence.StatusRecord -import au.gov.health.covidsafe.status.persistence.StatusRecordStorage -import au.gov.health.covidsafe.streetpass.ConnectionRecord -import au.gov.health.covidsafe.streetpass.StreetPassScanner -import au.gov.health.covidsafe.streetpass.StreetPassServer -import au.gov.health.covidsafe.streetpass.StreetPassWorker +import au.gov.health.covidsafe.sensor.Sensor +import au.gov.health.covidsafe.sensor.SensorArray +import au.gov.health.covidsafe.sensor.SensorDelegate +import au.gov.health.covidsafe.sensor.ble.BLEDevice +import au.gov.health.covidsafe.sensor.ble.BLESensorConfiguration +import au.gov.health.covidsafe.sensor.ble.BLE_TxPower +import au.gov.health.covidsafe.sensor.datatype.* +import au.gov.health.covidsafe.sensor.payload.PayloadDataSupplier import au.gov.health.covidsafe.streetpass.persistence.Encryption import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase -import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_DEVICE -import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_RSSI -import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_TXPOWER -import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.ENCRYPTED_EMPTY_DICT -import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.VERSION_ONE import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage import au.gov.health.covidsafe.ui.utils.LocalBlobV2 import au.gov.health.covidsafe.ui.utils.Utils -import com.google.gson.Gson -import com.google.gson.GsonBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import pub.devrel.easypermissions.EasyPermissions import java.lang.ref.WeakReference +import java.util.* +import kotlin.collections.HashMap import kotlin.coroutines.CoroutineContext private const val POWER_SAVE_WHITELIST_CHANGED = "android.os.action.POWER_SAVE_WHITELIST_CHANGED" @Keep -class BluetoothMonitoringService : LifecycleService(), CoroutineScope { +class BluetoothMonitoringService : LifecycleService(), CoroutineScope, SensorDelegate, PayloadDataSupplier { @Keep - private lateinit var serviceUUID: String - - private var streetPassServer: StreetPassServer? = null - private var streetPassScanner: StreetPassScanner? = null - private var advertiser: BLEAdvertiser? = null - - private var worker: StreetPassWorker? = null - - private val streetPassReceiver = StreetPassReceiver() - private val statusReceiver = StatusReceiver() private val bluetoothStatusReceiver = BluetoothStatusReceiver() - private lateinit var streetPassRecordStorage: StreetPassRecordStorage - private lateinit var statusRecordStorage: StatusRecordStorage - private var job: Job = Job() override val coroutineContext: CoroutineContext @@ -86,52 +63,42 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { private lateinit var commandHandler: CommandHandler - private lateinit var localBroadcastManager: LocalBroadcastManager - private val awsClient = NetworkFactory.awsClient - private val gson: Gson = GsonBuilder().disableHtmlEscaping().create() + // Sensor for proximity detection + private var sensor: Sensor? = null + private lateinit var streetPassRecordStorage: StreetPassRecordStorage + private var appDelegate: BluetoothMonitoringService = this + + private var recentSaves: MutableMap = HashMap() override fun onCreate() { super.onCreate() - localBroadcastManager = LocalBroadcastManager.getInstance(this) + AppContext = applicationContext setup() } private fun setup() { + streetPassRecordStorage = StreetPassRecordStorage(applicationContext) val pm = getSystemService(Context.POWER_SERVICE) as PowerManager CentralLog.setPowerManager(pm) - commandHandler = CommandHandler(WeakReference(this)) - CentralLog.d(TAG, "Creating service - BluetoothMonitoringService") - serviceUUID = BuildConfig.BLE_SSID - - worker = StreetPassWorker(this.applicationContext) - + broadcastMessage = Utils.retrieveBroadcastMessage(this.applicationContext) unregisterReceivers() registerReceivers() - streetPassRecordStorage = StreetPassRecordStorage(this.applicationContext) - statusRecordStorage = StatusRecordStorage(this.applicationContext) - PrivacyCleanerReceiver.startAlarm(this.applicationContext) setupNotifications() - broadcastMessage = Utils.retrieveBroadcastMessage(this.applicationContext) } fun teardown() { - streetPassServer?.tearDown() - streetPassServer = null - - streetPassScanner?.stopScan() - streetPassScanner = null - commandHandler.removeCallbacksAndMessages(null) Utils.cancelBMUpdateCheck(this.applicationContext) Utils.cancelNextScan(this.applicationContext) Utils.cancelNextAdvertise(this.applicationContext) + sensor?.stop() } private fun setupNotifications() { @@ -226,31 +193,18 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { fun runService(cmd: Command?) { CentralLog.i(TAG, "Command is:${cmd?.string}") - - //check for permissions - if (!isLocationPermissionEnabled() || !isBluetoothEnabled()) { - CentralLog.i( - TAG, - "location permission: ${isLocationPermissionEnabled()} bluetooth: ${isBluetoothEnabled()}" - ) - showForegroundNotification() - return - } - + when (cmd) { Command.ACTION_START -> { - setupService() actionStart() Utils.scheduleNextHealthCheck(this.applicationContext, healthCheckInterval) Utils.scheduleBMUpdateCheck(this.applicationContext, bmCheckInterval) } Command.ACTION_SCAN -> { - actionScan() } Command.ACTION_ADVERTISE -> { - actionAdvertise() } Command.ACTION_UPDATE_BM -> { @@ -269,17 +223,6 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { } } - private fun actionStop() { - stopForeground(true) - stopSelf() - CentralLog.w(TAG, "Service Stopping") - } - - private fun actionHealthCheck() { - Utils.scheduleNextHealthCheck(this.applicationContext, healthCheckInterval) - performHealthCheck() - } - private fun actionStart() { if (Preference.isOnBoarded(this)) { CentralLog.d(TAG, "Service Starting ") @@ -299,17 +242,47 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { params = null, onSuccess = { broadcastMessage = it.tempId - setupCycles() + sensorStart() }, onFailure = { } ) - } else if (Preference.isOnBoarded(this)) { - setupCycles() } + sensorStart() } } + fun sensorStart() { + if (broadcastMessage != null) { + streetPassRecordStorage = StreetPassRecordStorage(applicationContext) + sensor = SensorArray(applicationContext, this) + getAppDelegate().sensor()?.add(this) + // Sensor will start and stop with Bluetooth power on / off events + sensor?.start() + } + } + + /// Get app delegate + fun getAppDelegate(): BluetoothMonitoringService { + return appDelegate + } + + /// Get sensor + fun sensor(): Sensor? { + return sensor + } + + private fun actionStop() { + stopForeground(true) + stopSelf() + CentralLog.w(TAG, "Service Stopping") + } + + private fun actionHealthCheck() { + Utils.scheduleNextHealthCheck(this.applicationContext, healthCheckInterval) + performHealthCheck() + } + private fun actionUpdateBm() { Utils.scheduleBMUpdateCheck(this.applicationContext, bmCheckInterval) @@ -330,102 +303,6 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { } } - private fun calcPhaseShift(min: Long, max: Long): Long { - return (min + (Math.random() * (max - min))).toLong() - } - - private fun actionScan() { - if (Preference.isOnBoarded(this) && Utils.needToUpdate(this.applicationContext) || broadcastMessage == null) { - //need to pull new BM - UpdateBroadcastMessageAndPerformScanWithExponentialBackOff(awsClient, applicationContext, lifecycle).invoke( - params = null, - onSuccess = { - broadcastMessage = it.tempId - performScanAndScheduleNextScan() - }, - onFailure = { - } - ) - } else if (Preference.isOnBoarded(this)) { - performScanAndScheduleNextScan() - } - } - - private fun actionAdvertise() { - setupAdvertiser() - - if (isBluetoothEnabled()) { - advertiser?.startAdvertising(advertisingDuration) - } else { - CentralLog.w(TAG, "Unable to start advertising, bluetooth is off") - } - - commandHandler.scheduleNextAdvertise(advertisingDuration + advertisingGap) - } - - private fun setupService() { - streetPassServer = - streetPassServer ?: StreetPassServer(this.applicationContext, serviceUUID) - setupScanner() - setupAdvertiser() - } - - private fun setupScanner() { - streetPassScanner = streetPassScanner ?: StreetPassScanner( - this, - serviceUUID, - scanDuration - ) - } - - private fun setupAdvertiser() { - advertiser = advertiser ?: BLEAdvertiser(serviceUUID) - } - - private fun setupCycles() { - setupScanCycles() - setupAdvertisingCycles() - } - - private fun setupScanCycles() { - actionScan() - } - - private fun setupAdvertisingCycles() { - actionAdvertise() - } - - private fun performScanAndScheduleNextScan() { - - setupScanner() - - commandHandler.scheduleNextScan( - scanDuration + calcPhaseShift( - minScanInterval, - maxScanInterval - ) - ) - - startScan() - - } - - private fun startScan() { - - if (isBluetoothEnabled()) { - - streetPassScanner?.let { scanner -> - if (!scanner.isScanning()) { - scanner.startScan() - } else { - CentralLog.e(TAG, "Already scanning!") - } - } - } else { - CentralLog.w(TAG, "Unable to start scan - bluetooth is off") - } - } - private fun performHealthCheck() { CentralLog.i(TAG, "Performing self diagnosis") @@ -443,29 +320,6 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { CHANNEL_ID ) ) - - //ensure our service is there - setupService() - - if (!commandHandler.hasScanScheduled()) { - CentralLog.w(TAG, "Missing Scan Schedule - rectifying") - setupScanCycles() - } else { - CentralLog.w(TAG, "Scan Schedule present") - } - - if (!commandHandler.hasAdvertiseScheduled()) { - CentralLog.w(TAG, "Missing Advertise Schedule - rectifying") - setupAdvertisingCycles() - } else { - CentralLog.w( - TAG, - "Advertise Schedule present. Should be advertising?: ${ - advertiser?.shouldBeAdvertising - ?: false - }. Is Advertising?: ${advertiser?.isAdvertising ?: false}" - ) - } } override fun onDestroy() { @@ -475,9 +329,6 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { teardown() unregisterReceivers() - worker?.terminateConnections() - worker?.unregisterReceivers() - job.cancel() CentralLog.i(TAG, "BluetoothMonitoringService destroyed") @@ -540,43 +391,7 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { } } - private fun registerLocationChangeReceiver() { - registerReceiver(gpsSwitchStateReceiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)) - } - - private fun registerPowerModeChangeReceiver() { - registerReceiver(powerStateChangeReceiver, IntentFilter(POWER_SAVE_WHITELIST_CHANGED)) - } - - private fun registerReceivers() { - val recordAvailableFilter = IntentFilter(ACTION_RECEIVED_STREETPASS) - localBroadcastManager.registerReceiver(streetPassReceiver, recordAvailableFilter) - - val statusReceivedFilter = IntentFilter(ACTION_RECEIVED_STATUS) - localBroadcastManager.registerReceiver(statusReceiver, statusReceivedFilter) - - val bluetoothStatusReceivedFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) - registerReceiver(bluetoothStatusReceiver, bluetoothStatusReceivedFilter) - - registerLocationChangeReceiver() - registerPowerModeChangeReceiver() - - CentralLog.i(TAG, "Receivers registered") - } - private fun unregisterReceivers() { - try { - localBroadcastManager.unregisterReceiver(streetPassReceiver) - } catch (e: Throwable) { - CentralLog.w(TAG, "streetPassReceiver is not registered?") - } - - try { - localBroadcastManager.unregisterReceiver(statusReceiver) - } catch (e: Throwable) { - CentralLog.w(TAG, "statusReceiver is not registered?") - } - try { unregisterReceiver(bluetoothStatusReceiver) @@ -604,6 +419,24 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { } } + private fun registerLocationChangeReceiver() { + registerReceiver(gpsSwitchStateReceiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)) + } + + private fun registerPowerModeChangeReceiver() { + registerReceiver(powerStateChangeReceiver, IntentFilter(POWER_SAVE_WHITELIST_CHANGED)) + } + + private fun registerReceivers() { + val bluetoothStatusReceivedFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) + registerReceiver(bluetoothStatusReceiver, bluetoothStatusReceivedFilter) + + registerLocationChangeReceiver() + registerPowerModeChangeReceiver() + + CentralLog.i(TAG, "Receivers registered") + } + inner class BluetoothStatusReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -634,83 +467,6 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { } } - inner class StreetPassReceiver : BroadcastReceiver() { - - private val TAG = "StreetPassReceiver" - - override fun onReceive(context: Context, intent: Intent) { - - if (ACTION_RECEIVED_STREETPASS == intent.action) { - val connRecord: ConnectionRecord? = intent.getParcelableExtra(STREET_PASS) - CentralLog.d(TAG, "StreetPass received: $connRecord") - - if (connRecord != null && connRecord.msg.isNotEmpty()) { - - val remoteBlob: String = if (connRecord.version == VERSION_ONE) { - with(receiver = connRecord) { - val plainRecordByteArray = gson.toJson(StreetPassRecordDatabase.Companion.EncryptedRecord( - peripheral.modelP, central.modelC, rssi, txPower, msg = msg)) - .toByteArray(Charsets.UTF_8) - Encryption.encryptPayload(plainRecordByteArray) - } - } else { - //For version after version 1, the message is already encrypted in msg and we can store it as remote BLOB - connRecord.msg - } - val localBlob: String = if (connRecord.version == VERSION_ONE) { - ENCRYPTED_EMPTY_DICT - } else { - with(receiver = connRecord) { - val modelP = if (DUMMY_DEVICE == peripheral.modelP) null else peripheral.modelP - val modelC = if (DUMMY_DEVICE == central.modelC) null else central.modelC - val rssi = if (rssi == DUMMY_RSSI) null else rssi - val txPower = if (txPower == DUMMY_TXPOWER) null else txPower - val plainLocalBlob = gson.toJson(LocalBlobV2(modelP, modelC, rssi, txPower)) - .toByteArray(Charsets.UTF_8) - Encryption.encryptPayload(plainLocalBlob) - } - } - - val record = StreetPassRecord( - v = if (connRecord.version == 1) TracerApp.protocolVersion else (connRecord.version), - org = connRecord.org, - localBlob = localBlob, - remoteBlob = remoteBlob - ) - - launch { - CentralLog.d( - TAG, - "Coroutine - Saving StreetPassRecord: ${Utils.getDate(record.timestamp)} $record") - - streetPassRecordStorage.saveRecord(record) - } - } - } - } - } - - inner class StatusReceiver : BroadcastReceiver() { - private val TAG = "StatusReceiver" - - override fun onReceive(context: Context, intent: Intent) { - - if (ACTION_RECEIVED_STATUS == intent.action) { - val status: Status? = intent.getParcelableExtra(STATUS) - status?.let { - CentralLog.d(TAG, "Status received: ${it.msg}") - - if (it.msg.isNotEmpty()) { - val statusRecord = StatusRecord(it.msg) - launch { - statusRecordStorage.saveRecord(statusRecord) - } - } - } - } - } - } - enum class Command(val index: Int, val string: String) { INVALID(-1, "INVALID"), ACTION_START(0, "START"), @@ -726,6 +482,128 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { } } + override fun sensor(sensor: SensorType?, didDetect: TargetIdentifier?) { + CentralLog.d(TAG, "${sensor?.name} ,didDetect= $didDetect") + } + + override fun sensor(sensor: SensorType?, didRead: PayloadData?, fromTarget: TargetIdentifier?) { + CentralLog.d(TAG, "${sensor?.name} ,didRead= ${didRead?.shortName()} ,fromTarget= $fromTarget") + } + + override fun sensor(sensor: SensorType?, didShare: MutableList?, fromTarget: TargetIdentifier?) { + val payloads: MutableList = ArrayList(didShare!!.size) + for (payloadData in didShare) { + payloads.add(payloadData.shortName()) + } + CentralLog.d(TAG, "${sensor?.name} ,didShare= $payloads ,fromTarget= $fromTarget") + } + + override fun sensor(sensor: SensorType?, didMeasure: Proximity?, fromTarget: TargetIdentifier?) { + CentralLog.d(TAG, "${sensor?.name} ,didMeasure= ${didMeasure?.description()} ,fromTarget= $fromTarget") + } + + override fun sensor(sensor: SensorType?, didVisit: Location?) { + CentralLog.d(TAG, "${sensor?.name} ,didVisit= ${didVisit?.description()}") + } + + override fun sensor(sensor: SensorType?, didMeasure: Proximity?, fromTarget: TargetIdentifier?, withPayload: PayloadData?, device: BLEDevice) { + CentralLog.d(TAG, "${sensor?.name} ,didMeasure= ${didMeasure?.description()} ,fromTarget= ${fromTarget} ,withPayload= ${withPayload?.shortName()}, withDevice=$device") + wrtieEncounterRecordToDB(device) + } + + override fun sensor(sensor: SensorType?, didMeasure: Proximity?, fromTarget: TargetIdentifier?, withPayload: PayloadData?){ + CentralLog.d(TAG, "${sensor?.name} ,didMeasure= ${didMeasure?.description()} ,fromTarget= ${fromTarget} ,withPayload= ${withPayload?.shortName()}") + } + + override fun sensor(sensor: SensorType?, didUpdateState: SensorState?) { + CentralLog.d(TAG, "${sensor?.name} ,didUpdateState= ${didUpdateState?.name}") + } + + override fun sensor(sensor: SensorType?, didRead: PayloadData?, fromTarget: TargetIdentifier?, atProximity: Proximity?, withTxPower: Int, device: BLEDevice) { + CentralLog.d(TAG, "${sensor?.name} ,fromTarget= $fromTarget , atProximity= ${atProximity}, withTxPower= $withTxPower") + //wrtieEncounterRecordToDB(device) + } + + private fun cleanRecentSaves() { + recentSaves = recentSaves.filter { (key, value) -> TimeInterval( Date().time - value.time).value < BuildConfig.PERIPHERAL_PAYLOAD_SAVE_INTERVAL} as MutableMap + } + + private fun wrtieEncounterRecordToDB(device: BLEDevice): Any { + + return try { + + var deviceId: String = if(device.pseudoDeviceAddress()==null)device.identifier.value else device.pseudoDeviceAddress().toString() + cleanRecentSaves() + if(device.payloadData() != null && !recentSaves.containsKey(deviceId)) { + recentSaves.put(deviceId,Date()) + + val didRead = device.payloadData() + val deviceRssi = device.rssi() + val withTxPower = device.txPower() + + val peripheralrecord = ReadRequestPayload.gson.fromJson(String(didRead.value), ReadRequestPayload::class.java) + val modelC = if (StreetPassRecordDatabase.DUMMY_DEVICE == TracerApp.asCentralDevice().modelC) null else TracerApp.asCentralDevice().modelC + val modelP = if (StreetPassRecordDatabase.DUMMY_DEVICE == peripheralrecord.modelP) null else peripheralrecord.modelP + val rssi = if (deviceRssi?.value == StreetPassRecordDatabase.DUMMY_RSSI) null else deviceRssi.value + val txPower = if (withTxPower == null || withTxPower.value == StreetPassRecordDatabase.DUMMY_TXPOWER) null else withTxPower.value + val plainLocalBlob = ReadRequestPayload.gson.toJson(LocalBlobV2( + modelP, + modelC, + txPower, + rssi + )).toByteArray(Charsets.UTF_8) + + val localBlob = Encryption.encryptPayload(plainLocalBlob) + val record = StreetPassRecord( + v = peripheralrecord.v, + org = peripheralrecord.org, + localBlob = localBlob, + remoteBlob = peripheralrecord.msg + ) + + launch { + streetPassRecordStorage.saveRecord(record) + } + }else{} + } catch (e: java.lang.Exception) { + CentralLog.d(TAG, "Json parsing failed = $e") + } + } + + override fun payload(data: Data?): MutableList { + // Split raw data comprising of concatenated payloads into individual payloads, in our case we will only every get one payload at a time + val payload = PayloadData(data?.value) + val payloads: MutableList = ArrayList() + payloads.add(payload) + return payloads + } + + inner class ReadRequestEncryptedPayload(val timestamp: Long, val modelP: String, val msg: String?) + + override fun payload(timestamp: PayloadTimestamp?): PayloadData { + val peripheral = TracerApp.asPeripheralDevice() + val readRequest = ReadRequestEncryptedPayload( + System.currentTimeMillis() / 1000L, + peripheral.modelP, + thisDeviceMsg() + ) + val plainRecord = ReadRequestPayload.gson.toJson(readRequest) + + CentralLog.d(TAG, "onCharacteristicReadRequest plainRecord = $plainRecord") + + val plainRecordByteArray = plainRecord.toByteArray(Charsets.UTF_8) + val remoteBlob = Encryption.encryptPayload(plainRecordByteArray) + val base = + ReadRequestPayload( + v = TracerApp.protocolVersion, + msg = remoteBlob, + org = TracerApp.ORG, + modelP = null //This is going to be stored as empty in the db as DUMMY value + ).getPayload() + val value = base.copyOfRange(0, base.size) + return PayloadData(value) + } + companion object { private const val TAG = "BTMService" @@ -744,25 +622,22 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { const val PENDING_WIZARD_REQ_CODE = 10 const val PENDING_BM_UPDATE = 11 const val PENDING_PRIVACY_CLEANER_CODE = 12 - const val DAILY_UPLOAD_NOTIFICATION_CODE = 13 - var broadcastMessage: String? = null - - const val scanDuration: Long = BuildConfig.SCAN_DURATION - const val minScanInterval: Long = BuildConfig.MIN_SCAN_INTERVAL - const val maxScanInterval: Long = BuildConfig.MAX_SCAN_INTERVAL - - const val advertisingDuration: Long = BuildConfig.ADVERTISING_DURATION - const val advertisingGap: Long = BuildConfig.ADVERTISING_INTERVAL - const val maxQueueTime: Long = BuildConfig.MAX_QUEUE_TIME const val bmCheckInterval: Long = BuildConfig.BM_CHECK_INTERVAL const val healthCheckInterval: Long = BuildConfig.HEALTH_CHECK_INTERVAL - const val connectionTimeout: Long = BuildConfig.CONNECTION_TIMEOUT const val blacklistDuration: Long = BuildConfig.BLACKLIST_DURATION - + lateinit var AppContext: Context + fun thisDeviceMsg(): String? { + broadcastMessage?.let { + CentralLog.i(TAG, "Retrieved BM for storage: $it") + return it + } + CentralLog.e(TAG, "No local Broadcast Message") + return "" + } } } diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/IBluetoothGattInvocationHandler.java b/app/src/main/java/au/gov/health/covidsafe/streetpass/IBluetoothGattInvocationHandler.java index e916cea..d92c278 100644 --- a/app/src/main/java/au/gov/health/covidsafe/streetpass/IBluetoothGattInvocationHandler.java +++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/IBluetoothGattInvocationHandler.java @@ -4,7 +4,7 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import au.gov.health.covidsafe.logging.CentralLog; - /* This class is used by StreetPassPairingFix to proxy calls between BluetoothGatt and the underlying +/* This class is used by StreetPassPairingFix to proxy calls between BluetoothGatt and the underlying * IBluetoothGatt implementation (the interface between Android SDK's bluetooth API and the system * bluetooth daemon) diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassPairingFix.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassPairingFix.kt index fd71aab..eb15f1f 100644 --- a/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassPairingFix.kt +++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassPairingFix.kt @@ -149,20 +149,26 @@ object StreetPassPairingFix { // Instance - this is what is called to initiate a read/write to a preipheral val mService: Object = mServiceField!!.get(gatt) as Object - // Wrap the IBLuetoothGatt instance in a Proxy object in order to intercept calls to - // readCharacteristic and writeCharacteristic. IBluetoothGattInvocationHandler will catch - // calls to these functions and rewrite their authReq field to ensure no pairing attempts - // occur - val mServiceProxy = Proxy.newProxyInstance(gatt.javaClass.classLoader, - Array(1) { iBluetoothGattClass!! }, - IBluetoothGattInvocationHandler(mService)) - - // Write the proxy back to BluetoothGatt.mService - mServiceField!!.set(gatt, mServiceProxy) + // Ensure that mService isn't already a proxy, for instance if this method was called + // twice on the same BluetoothGatt instance. + if (Proxy.isProxyClass(mService.javaClass)) { + CentralLog.i(TAG, + "Not proxying this mService as it is already proxied!") + } else { + // Wrap the IBluetoothGatt instance in a Proxy object in order to intercept calls to + // readCharacteristic and writeCharacteristic. IBluetoothGattInvocationHandler will catch + // calls to these functions and rewrite their authReq field to ensure no pairing attempts + // occur + val mServiceProxy = Proxy.newProxyInstance(gatt.javaClass.classLoader, + Array(1) { iBluetoothGattClass!! }, + IBluetoothGattInvocationHandler(mService)) + // Write the proxy back to BluetoothGatt.mService + mServiceField!!.set(gatt, mServiceProxy) + } // Reset accessibility mServiceField!!.isAccessible = mServiceAccessible - } + } catch (e: IllegalAccessException) { // Field was inaccessible when written CentralLog.i(TAG, diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassServer.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassServer.kt deleted file mode 100644 index 80e5401..0000000 --- a/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassServer.kt +++ /dev/null @@ -1,32 +0,0 @@ -package au.gov.health.covidsafe.streetpass - -import android.content.Context -import au.gov.health.covidsafe.bluetooth.gatt.GattServer -import au.gov.health.covidsafe.bluetooth.gatt.GattService - -class StreetPassServer constructor(val context: Context, serviceUUIDString: String) { - - private val TAG = "StreetPassServer" - private var gattServer: GattServer? = null - - init { - gattServer = setupGattServer(context, serviceUUIDString) - } - - private fun setupGattServer(context: Context, serviceUUIDString: String): GattServer? { - val gattServer = GattServer(context, serviceUUIDString) - val started = gattServer.startServer() - - if (started) { - val readService = GattService(context, serviceUUIDString) - gattServer.addService(readService) - return gattServer - } - return null - } - - fun tearDown() { - gattServer?.stop() - } - -} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassWorker.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassWorker.kt index 4b14549..febc8d4 100644 --- a/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassWorker.kt +++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassWorker.kt @@ -310,7 +310,7 @@ class StreetPassWorker(val context: Context) { it.checklist.started.status = true it.checklist.started.timePerformed = System.currentTimeMillis() - it.startWork(context, gattCallback) + it.startWork(context, gattCallback) var connecting = it.gatt?.connect() ?: false @@ -646,7 +646,7 @@ class StreetPassWorker(val context: Context) { } } - inner class EncryptedWriteRequestPayload(val timestamp: Long, val modelC: String, val rssi: Int, val txPower: Int?, val msg: String) + inner class EncryptedWriteRequestPayload(val timestamp: Long, val modelC: String, val rssi: Int, val txPower: Int?, val msg: String?) override fun onCharacteristicWrite( gatt: BluetoothGatt, diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordStorage.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordStorage.kt index 53c8536..ccf9591 100644 --- a/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordStorage.kt +++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordStorage.kt @@ -25,5 +25,4 @@ class StreetPassRecordStorage(val context: Context) { fun getAllRecords(): List { return recordDao.getCurrentRecords() } - } \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragment.kt index a54218c..e54b0c1 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragment.kt @@ -16,7 +16,6 @@ import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent -import androidx.constraintlayout.solver.GoalRow import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels @@ -100,10 +99,15 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks, Networ initializeRefreshButton() initializePullToRefresh() initialiseReRegistration() + setupHyperlink() NetworkConnectionCheck.addNetworkChangedListener(requireContext(), this) } + private fun setupHyperlink() { + txt_update_description.movementMethod = LinkMovementMethod.getInstance() + } + private fun initializeNoNetworkError() { no_network_error_text_view.setOnClickListener { startActivity(Intent(android.provider.Settings.ACTION_WIRELESS_SETTINGS)) @@ -235,6 +239,10 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks, Networ homeFragmentViewModel.collectionMessageVisible.value = false askForLocationPermission() } + btn_proceed.setOnClickListener { + layout_herald_upgrade.slideAnimation(SlideDirection.UP, SlideType.HIDE, 200) + homeFragmentViewModel.heraldUpgradeMessage.value = false + } } private fun initializeUploadTestDataNavigation() { diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragmentViewModel.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragmentViewModel.kt index dff0518..39914e8 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragmentViewModel.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragmentViewModel.kt @@ -23,6 +23,7 @@ class HomeFragmentViewModel(application: Application) : AndroidViewModel(applica val caseStatisticsLiveData = MutableLiveData() val isRefreshing = MutableLiveData() val collectionMessageVisible = MutableLiveData() + val heraldUpgradeMessage = MutableLiveData() // Show = true and hide = false val turnCaseNumber = MutableLiveData() lateinit var context: Context @@ -72,10 +73,13 @@ class HomeFragmentViewModel(application: Application) : AndroidViewModel(applica val latestVersion = Preference.getBuildNumber(context) // When We want to show disclaimer to user after update, minVersionShowPolicy should be as same as the current version val minVersionShowPolicy = 74 + val minVersionHeraldPolicy = 94 val currentVersion = BuildConfig.VERSION_CODE if (latestVersion == 0) { collectionMessageVisible.value = true + heraldUpgradeMessage.value = true } else { + heraldUpgradeMessage.value = currentVersion <= minVersionHeraldPolicy && currentVersion > latestVersion collectionMessageVisible.value = currentVersion <= minVersionShowPolicy && currentVersion > latestVersion } Preference.putBuildNumber(context, currentVersion) diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionFragment.kt index 1f6b140..d030242 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionFragment.kt @@ -12,27 +12,25 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent +import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import au.gov.health.covidsafe.HomeActivity -import au.gov.health.covidsafe.preference.Preference import au.gov.health.covidsafe.R import au.gov.health.covidsafe.app.TracerApp import au.gov.health.covidsafe.extensions.* +import au.gov.health.covidsafe.preference.Preference import au.gov.health.covidsafe.talkback.setHeading import au.gov.health.covidsafe.ui.base.PagerChildFragment import au.gov.health.covidsafe.ui.base.UploadButtonLayout import kotlinx.android.synthetic.main.fragment_permission.* import pub.devrel.easypermissions.EasyPermissions +import java.util.* +import kotlin.collections.ArrayList class PermissionFragment : PagerChildFragment(), EasyPermissions.PermissionCallbacks { companion object { - - val requiredPermissions = arrayOf( - Manifest.permission.BLUETOOTH, - Manifest.permission.BLUETOOTH_ADMIN, - Manifest.permission.ACCESS_FINE_LOCATION - ) + val requiredPermissions = PermissionFragment().requestPermissions() } override var step: Int? = 4 @@ -72,6 +70,20 @@ class PermissionFragment : PagerChildFragment(), EasyPermissions.PermissionCallb } else super.onActivityResult(requestCode, resultCode, data) } + private fun requestPermissions(): MutableList { + // Check and request permissions + val requiredPermissions: MutableList = ArrayList() + requiredPermissions.add(Manifest.permission.BLUETOOTH) + requiredPermissions.add(Manifest.permission.BLUETOOTH_ADMIN) + requiredPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + requiredPermissions.add(Manifest.permission.FOREGROUND_SERVICE) + } + requiredPermissions.add(Manifest.permission.WAKE_LOCK) + + return requiredPermissions + } + private fun navigateToNextPage() { navigationStarted = false if (hasAllPermissionsAndBluetoothOn()) { diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 186ddd8..20ecfd2 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -124,7 +124,8 @@ app:layout_anchorGravity="center" android:background="@color/white" android:elevation="12dp" - visibility="@{viewModel.collectionMessageVisible}"> + visibility="@{viewModel.collectionMessageVisible}" + android:visibility="gone"> + + + + + + + + + + + + +