mirror of
https://github.com/AU-COVIDSafe/mobile-android.git
synced 2025-01-18 16:56:34 +00:00
COVIDSafe code from version 2.0 (#38)
This commit is contained in:
parent
6bf46ded07
commit
26ca20fc3d
105 changed files with 6415 additions and 651 deletions
|
@ -29,8 +29,8 @@ android {
|
||||||
applicationId "au.gov.health.covidsafe"
|
applicationId "au.gov.health.covidsafe"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 80
|
versionCode 94
|
||||||
versionName "1.14.0"
|
versionName "2.0"
|
||||||
buildConfigField "String", "GITHASH", "\"${getGitHash()}\""
|
buildConfigField "String", "GITHASH", "\"${getGitHash()}\""
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
@ -62,6 +62,7 @@ android {
|
||||||
buildConfigField "long", "HEALTH_CHECK_INTERVAL", HEALTH_CHECK_INTERVAL
|
buildConfigField "long", "HEALTH_CHECK_INTERVAL", HEALTH_CHECK_INTERVAL
|
||||||
buildConfigField "long", "CONNECTION_TIMEOUT", CONNECTION_TIMEOUT
|
buildConfigField "long", "CONNECTION_TIMEOUT", CONNECTION_TIMEOUT
|
||||||
buildConfigField "long", "BLACKLIST_DURATION", BLACKLIST_DURATION
|
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_DURATION", ADVERTISING_DURATION
|
||||||
buildConfigField "long", "ADVERTISING_INTERVAL", ADVERTISING_INTERVAL
|
buildConfigField "long", "ADVERTISING_INTERVAL", ADVERTISING_INTERVAL
|
||||||
|
@ -84,6 +85,9 @@ android {
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
buildConfigField "String", "BLE_SSID", STAGING_SERVICE_UUID
|
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 "boolean", "ENABLE_DEBUG_SCREEN", "true"
|
||||||
buildConfigField "String", "END_POINT_PREFIX", TEST_END_POINT_PREFIX
|
buildConfigField "String", "END_POINT_PREFIX", TEST_END_POINT_PREFIX
|
||||||
buildConfigField "String", "BASE_URL", TEST_BASE_URL
|
buildConfigField "String", "BASE_URL", TEST_BASE_URL
|
||||||
|
@ -100,6 +104,9 @@ android {
|
||||||
|
|
||||||
staging {
|
staging {
|
||||||
buildConfigField "String", "BLE_SSID", STAGING_SERVICE_UUID
|
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 "boolean", "ENABLE_DEBUG_SCREEN", "true"
|
||||||
buildConfigField "String", "END_POINT_PREFIX", STAGING_END_POINT_PREFIX
|
buildConfigField "String", "END_POINT_PREFIX", STAGING_END_POINT_PREFIX
|
||||||
buildConfigField "String", "BASE_URL", STAGING_BASE_URL
|
buildConfigField "String", "BASE_URL", STAGING_BASE_URL
|
||||||
|
@ -125,6 +132,9 @@ android {
|
||||||
release {
|
release {
|
||||||
|
|
||||||
buildConfigField "String", "BLE_SSID", PRODUCTION_SERVICE_UUID
|
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", "END_POINT_PREFIX", PRODUCTION_END_POINT_PREFIX
|
||||||
buildConfigField "String", "BASE_URL", PROD_BASE_URL
|
buildConfigField "String", "BASE_URL", PROD_BASE_URL
|
||||||
buildConfigField "String", "IOS_BACKGROUND_UUID", PRODUCTION_BACKGROUND_IOS_SERVICE_UUID
|
buildConfigField "String", "IOS_BACKGROUND_UUID", PRODUCTION_BACKGROUND_IOS_SERVICE_UUID
|
||||||
|
|
|
@ -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')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<BLEAdvertManufacturerData> 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<Data> 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<Data> 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<Data> 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<Data> 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<Data> 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<Data> 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<Data> 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<Data> 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<Data> 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<Data> 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<Data> 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<Data> 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<Data> 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<Data> 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<Data> messages = BLEDeviceFilter.extractMessages(raw.value);
|
||||||
|
assertNull(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExtractMessages_MacbookProOverflow() throws Exception {
|
||||||
|
final Data raw = Data.fromHexEncodedString("02011a0aff4c001005031c0b4cac02011a0aff4c00100503");
|
||||||
|
final List<Data> 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<BLEDeviceFilter.FilterPattern> 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<Data> messages = BLEDeviceFilter.extractMessages(raw.value);
|
||||||
|
assertEquals(2, messages.size());
|
||||||
|
assertEquals("1006071EA3DD89E0", messages.get(0).hexEncodedString());
|
||||||
|
assertEquals("0100000000000000000000200000000000", messages.get(1).hexEncodedString());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,10 @@
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="au.gov.health.covidsafe.app.TracerApp"
|
android:name="au.gov.health.covidsafe.app.TracerApp"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
@ -27,6 +31,7 @@
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/MyTheme.DayNight"
|
android:theme="@style/MyTheme.DayNight"
|
||||||
|
android:foregroundServiceType="location"
|
||||||
tools:replace="android:supportsRtl">
|
tools:replace="android:supportsRtl">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
|
|
@ -11,17 +11,18 @@ import au.gov.health.covidsafe.networking.response.MessagesResponse
|
||||||
import au.gov.health.covidsafe.notifications.NotificationBuilder
|
import au.gov.health.covidsafe.notifications.NotificationBuilder
|
||||||
import au.gov.health.covidsafe.preference.Preference
|
import au.gov.health.covidsafe.preference.Preference
|
||||||
import au.gov.health.covidsafe.scheduler.GetMessagesScheduler
|
import au.gov.health.covidsafe.scheduler.GetMessagesScheduler
|
||||||
|
import au.gov.health.covidsafe.sensor.SensorDelegate
|
||||||
|
import au.gov.health.covidsafe.sensor.ble.BLEDevice
|
||||||
|
import au.gov.health.covidsafe.sensor.datatype.*
|
||||||
import au.gov.health.covidsafe.ui.devicename.DeviceNameChangePromptActivity
|
import au.gov.health.covidsafe.ui.devicename.DeviceNameChangePromptActivity
|
||||||
import au.gov.health.covidsafe.ui.utils.Utils
|
import au.gov.health.covidsafe.ui.utils.Utils
|
||||||
import au.gov.health.covidsafe.utils.NetworkConnectionCheck
|
import au.gov.health.covidsafe.utils.NetworkConnectionCheck
|
||||||
import com.google.android.gms.tasks.OnCompleteListener
|
import com.google.android.gms.tasks.OnCompleteListener
|
||||||
import com.google.firebase.iid.FirebaseInstanceId
|
import com.google.firebase.iid.FirebaseInstanceId
|
||||||
import kotlinx.android.synthetic.main.view_home_setup_incomplete.*
|
|
||||||
|
|
||||||
private const val TAG = "HomeActivity"
|
private const val TAG = "HomeActivity"
|
||||||
private const val UNAUTHORIZED = "Unauthorized"
|
private const val UNAUTHORIZED = "Unauthorized"
|
||||||
|
|
||||||
class HomeActivity : FragmentActivity(), NetworkConnectionCheck.NetworkConnectionListener {
|
class HomeActivity : FragmentActivity(), NetworkConnectionCheck.NetworkConnectionListener, SensorDelegate {
|
||||||
|
|
||||||
var isAppUpdateAvailableLiveData = MutableLiveData<Boolean>()
|
var isAppUpdateAvailableLiveData = MutableLiveData<Boolean>()
|
||||||
var appUpdateAvailableMessageResponseLiveData = MutableLiveData<MessagesResponse>()
|
var appUpdateAvailableMessageResponseLiveData = MutableLiveData<MessagesResponse>()
|
||||||
|
@ -136,4 +137,30 @@ class HomeActivity : FragmentActivity(), NetworkConnectionCheck.NetworkConnectio
|
||||||
previousInternetConnection = isAvailable
|
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<PayloadData>?, 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) {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -4,40 +4,41 @@ import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import au.gov.health.covidsafe.BuildConfig
|
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.logging.CentralLog
|
||||||
import au.gov.health.covidsafe.services.BluetoothMonitoringService
|
import au.gov.health.covidsafe.services.BluetoothMonitoringService
|
||||||
import au.gov.health.covidsafe.streetpass.CentralDevice
|
import au.gov.health.covidsafe.streetpass.CentralDevice
|
||||||
import au.gov.health.covidsafe.streetpass.PeripheralDevice
|
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() {
|
class TracerApp : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
AppContext = applicationContext
|
AppContext = this.applicationContext
|
||||||
FeedbackModule.init(this)
|
FeedbackModule.init(this)
|
||||||
|
|
||||||
// GetMessagesScheduler.scheduleGetMessagesJob()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
|
||||||
private const val TAG = "TracerApp"
|
private const val TAG = "TracerApp"
|
||||||
const val ORG = BuildConfig.ORG
|
const val ORG = BuildConfig.ORG
|
||||||
const val protocolVersion = BuildConfig.PROTOCOL_VERSION
|
const val protocolVersion = BuildConfig.PROTOCOL_VERSION
|
||||||
|
|
||||||
lateinit var AppContext: Context
|
lateinit var AppContext: Context
|
||||||
|
|
||||||
fun thisDeviceMsg(): String {
|
fun thisDeviceMsg(): String? {
|
||||||
|
val broadcastMessage = Utils.retrieveBroadcastMessage(AppContext)
|
||||||
|
if (broadcastMessage != null) {
|
||||||
|
return broadcastMessage
|
||||||
|
} else {
|
||||||
BluetoothMonitoringService.broadcastMessage?.let {
|
BluetoothMonitoringService.broadcastMessage?.let {
|
||||||
CentralLog.i(TAG, "Retrieved BM for storage: $it")
|
CentralLog.i(TAG, "Retrieved BM for storage: $it")
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
}
|
||||||
CentralLog.e(TAG, "No local Broadcast Message")
|
CentralLog.e(TAG, "No local Broadcast Message")
|
||||||
return BluetoothMonitoringService.broadcastMessage!!
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
fun asPeripheralDevice(): PeripheralDevice {
|
fun asPeripheralDevice(): PeripheralDevice {
|
||||||
|
@ -47,6 +48,5 @@ class TracerApp : Application() {
|
||||||
fun asCentralDevice(): CentralDevice {
|
fun asCentralDevice(): CentralDevice {
|
||||||
return CentralDevice(Build.MODEL, "SELF")
|
return CentralDevice(Build.MODEL, "SELF")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ class GattServer constructor(val context: Context, serviceUUIDString: String) {
|
||||||
val readRequest = ReadRequestEncryptedPayload(
|
val readRequest = ReadRequestEncryptedPayload(
|
||||||
System.currentTimeMillis() / 1000L,
|
System.currentTimeMillis() / 1000L,
|
||||||
peripheral.modelP,
|
peripheral.modelP,
|
||||||
TracerApp.thisDeviceMsg()
|
TracerApp.thisDeviceMsg().toString()
|
||||||
)
|
)
|
||||||
val plainRecord = gson.toJson(readRequest)
|
val plainRecord = gson.toJson(readRequest)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package au.gov.health.covidsafe.extensions
|
package au.gov.health.covidsafe.extensions
|
||||||
|
|
||||||
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
import android.Manifest.permission.*
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
|
@ -4,12 +4,10 @@ import android.app.Notification
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.RemoteViews
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import au.gov.health.covidsafe.HomeActivity
|
import au.gov.health.covidsafe.HomeActivity
|
||||||
import au.gov.health.covidsafe.R
|
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_ACTIVITY
|
||||||
import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.PENDING_WIZARD_REQ_CODE
|
import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.PENDING_WIZARD_REQ_CODE
|
||||||
|
|
||||||
|
|
|
@ -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<PayloadData> 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) {
|
||||||
|
}
|
||||||
|
}
|
17
app/src/main/java/au/gov/health/covidsafe/sensor/Sensor.java
Normal file
17
app/src/main/java/au/gov/health/covidsafe/sensor/Sensor.java
Normal file
|
@ -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();
|
||||||
|
}
|
|
@ -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<Sensor> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<PayloadData> 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);
|
||||||
|
}
|
|
@ -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<BLEDevice> devices();
|
||||||
|
|
||||||
|
/// Delete
|
||||||
|
void delete(TargetIdentifier identifier);
|
||||||
|
|
||||||
|
/// Get payload sharing data for a peer
|
||||||
|
PayloadSharingData payloadSharingData(BLEDevice peer);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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<byte[]> signalCharacteristicWriteQueue = null;
|
||||||
|
|
||||||
|
/// Track connection timestamps
|
||||||
|
private Date lastDiscoveredAt = null;
|
||||||
|
private Date lastConnectedAt = null;
|
||||||
|
|
||||||
|
/// Payload data already shared with this peer
|
||||||
|
protected final List<PayloadData> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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<SensorDelegate> delegates = new ConcurrentLinkedQueue<>();
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
}
|
|
@ -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"
|
||||||
|
};
|
||||||
|
}
|
|
@ -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
|
||||||
|
* <p>
|
||||||
|
* Test impact of power management by ...
|
||||||
|
* <p>
|
||||||
|
* 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
|
||||||
|
* <p>
|
||||||
|
* 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
|
||||||
|
* <p>
|
||||||
|
* 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
|
||||||
|
* <p>
|
||||||
|
* 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
|
||||||
|
* <p>
|
||||||
|
* 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<BLETimerDelegate> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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<SensorDelegate> delegates = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current payload.
|
||||||
|
*/
|
||||||
|
PayloadData payloadData();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is transmitter supported.
|
||||||
|
*
|
||||||
|
* @return True if BLE advertising is supported.
|
||||||
|
*/
|
||||||
|
boolean isSupported();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<BluetoothStateManagerDelegate> delegates = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
BluetoothState state();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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<BLEDatabaseDelegate> delegates = new ConcurrentLinkedQueue<>();
|
||||||
|
private final Map<TargetIdentifier, BLEDevice> 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<BLEDevice> 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<BLEDevice>() {
|
||||||
|
@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<BLEDevice> 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<BLEDevice>() {
|
||||||
|
@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<BLEDevice> 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<BLEDevice> unknownDevices = new ArrayList<>();
|
||||||
|
final List<BLEDevice> 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<BLEDevice> devices = new ArrayList<>();
|
||||||
|
Collections.sort(unknownDevices, new Comparator<BLEDevice>() {
|
||||||
|
@Override
|
||||||
|
public int compare(BLEDevice d0, BLEDevice d1) {
|
||||||
|
return Long.compare(d1.lastUpdatedAt.getTime(), d0.lastUpdatedAt.getTime());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Collections.sort(knownDevices, new Comparator<BLEDevice>() {
|
||||||
|
@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<PayloadData> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ScanResult> = 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<ScanResult>) {
|
||||||
|
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<Boolean?> { 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<Boolean?> { 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<Boolean?> {
|
||||||
|
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<Boolean?>?) {
|
||||||
|
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<ScanFilter> = 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<Boolean?>) {
|
||||||
|
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<Boolean?>) {
|
||||||
|
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<BLEDevice> {
|
||||||
|
// Take current copy of concurrently modifiable scan results
|
||||||
|
val scanResultList: MutableList<ScanResult> = 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<BLEDevice> = HashSet()
|
||||||
|
val devices: MutableList<BLEDevice> = 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<BLEDevice> = 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<BLEDevice>) {
|
||||||
|
// 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<ByteArray> {
|
||||||
|
val fragments: Queue<ByteArray> = 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
|
||||||
|
}
|
|
@ -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<SensorDelegate> 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<PayloadData, Date> 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<PayloadData, Date> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Boolean> {
|
||||||
|
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<Triple<Boolean, AdvertiseCallback?, BluetoothGattServer?>>) {
|
||||||
|
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<Boolean>) {
|
||||||
|
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<BluetoothGattServer?>(null)
|
||||||
|
val callback: BluetoothGattServerCallback = object : BluetoothGattServerCallback() {
|
||||||
|
|
||||||
|
//this should be a table
|
||||||
|
//in order to handle many connections from different mac addresses
|
||||||
|
//val writeDataPayload: MutableMap<String, ByteArray> = HashMap()
|
||||||
|
//val readPayloadMap: MutableMap<String, ByteArray> = HashMap()
|
||||||
|
|
||||||
|
////in order to handle many connections from different mac addresses
|
||||||
|
//Implement as Same as GattServer in Covid here
|
||||||
|
private val onCharacteristicReadPayloadData: MutableMap<String, PayloadData?> = ConcurrentHashMap()
|
||||||
|
private val onCharacteristicWriteSignalData: MutableMap<String, ByteArray> = 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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<BLEAdvertSegment> extractSegments(byte[] raw, int offset) {
|
||||||
|
int position = offset;
|
||||||
|
ArrayList<BLEAdvertSegment> segments = new ArrayList<BLEAdvertSegment>();
|
||||||
|
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<BLEAdvertSegment> 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<BLEAdvertManufacturerData> extractManufacturerData(List<BLEAdvertSegment> segments) {
|
||||||
|
// find the manufacturerData code segment in the list
|
||||||
|
List<BLEAdvertManufacturerData> 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<BLEAdvertAppleManufacturerSegment> extractAppleManufacturerSegments(List<BLEAdvertManufacturerData> manuData) {
|
||||||
|
final List<BLEAdvertAppleManufacturerSegment> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, BLEAdvertSegmentType> BY_LABEL = new HashMap<>();
|
||||||
|
private static final Map<Integer, BLEAdvertSegmentType> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<FilterPattern> filterPatterns;
|
||||||
|
private final Map<Data, ShouldIgnore> 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<FilterPattern> 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<FilterPattern> compilePatterns(final String[] regularExpressions) {
|
||||||
|
final SensorLogger logger = new ConcreteSensorLogger("Sensor", "BLE.BLEDeviceFilter");
|
||||||
|
final List<FilterPattern> 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<Data> 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<BLEAdvertManufacturerData> bleAdvertManufacturerDataList = BLEAdvertParser.extractManufacturerData(bleScanResponseData.segments);
|
||||||
|
// Parse manufacturer specific data into messages
|
||||||
|
if (bleAdvertManufacturerDataList == null || bleAdvertManufacturerDataList.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final List<BLEAdvertAppleManufacturerSegment> bleAdvertAppleManufacturerSegments = BLEAdvertParser.extractAppleManufacturerSegments(bleAdvertManufacturerDataList);
|
||||||
|
// Convert segments to messages
|
||||||
|
if (bleAdvertAppleManufacturerSegments == null || bleAdvertAppleManufacturerSegments.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final List<Data> 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<Data> extractFeatures(final ScanRecord scanRecord) {
|
||||||
|
if (scanRecord == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Get message data
|
||||||
|
final List<Data> featureList = new ArrayList<>();
|
||||||
|
final List<Data> 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<FilterPattern> 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<Data> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<BLEAdvertSegment> segments;
|
||||||
|
|
||||||
|
public BLEScanResponseData(int dataLength, List<BLEAdvertSegment> segments) {
|
||||||
|
this.dataLength = dataLength;
|
||||||
|
this.segments = segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return segments.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<PayloadData> 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()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, String> 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<String> 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<PayloadData> didShare, TargetIdentifier fromTarget) {
|
||||||
|
for (PayloadData payloadData : didShare) {
|
||||||
|
if (payloads.put(payloadData.shortName(), fromTarget.value) == null) {
|
||||||
|
logger.debug("didShare (payload={})", payloadData.shortName());
|
||||||
|
write();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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<TargetIdentifier, String> identifierToPayload = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Date> payloadToTime = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Sample> 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<String> 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<PayloadData> didShare, TargetIdentifier fromTarget) {
|
||||||
|
for (PayloadData payload : didShare) {
|
||||||
|
add(payload.shortName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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<T> {
|
||||||
|
void accept(T value);
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 + "]";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 + ")";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() + "]";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright 2020 VMware, Inc.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
|
||||||
|
package au.gov.health.covidsafe.sensor.datatype;
|
||||||
|
|
||||||
|
public class Triple<A, B, C> {
|
||||||
|
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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright 2020 VMware, Inc.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
|
||||||
|
package au.gov.health.covidsafe.sensor.datatype;
|
||||||
|
|
||||||
|
public class Tuple<A, B> {
|
||||||
|
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 + ")";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 + ")";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 + ")";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<PayloadData> 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<PayloadData> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<PayloadData> payload(Data data);
|
||||||
|
}
|
|
@ -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<Integer, Notification> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, String, Notification> 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<Integer, Notification> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,71 +14,48 @@ import android.os.PowerManager
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
|
||||||
import au.gov.health.covidsafe.BuildConfig
|
import au.gov.health.covidsafe.BuildConfig
|
||||||
import au.gov.health.covidsafe.R
|
import au.gov.health.covidsafe.R
|
||||||
import au.gov.health.covidsafe.app.TracerApp
|
import au.gov.health.covidsafe.app.TracerApp
|
||||||
import au.gov.health.covidsafe.bluetooth.BLEAdvertiser
|
import au.gov.health.covidsafe.bluetooth.gatt.ReadRequestPayload
|
||||||
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.extensions.isLocationEnabledOnDevice
|
import au.gov.health.covidsafe.extensions.isLocationEnabledOnDevice
|
||||||
import au.gov.health.covidsafe.factory.NetworkFactory
|
import au.gov.health.covidsafe.factory.NetworkFactory
|
||||||
import au.gov.health.covidsafe.interactor.usecase.UpdateBroadcastMessageAndPerformScanWithExponentialBackOff
|
import au.gov.health.covidsafe.interactor.usecase.UpdateBroadcastMessageAndPerformScanWithExponentialBackOff
|
||||||
import au.gov.health.covidsafe.logging.CentralLog
|
import au.gov.health.covidsafe.logging.CentralLog
|
||||||
import au.gov.health.covidsafe.notifications.NotificationTemplates
|
import au.gov.health.covidsafe.notifications.NotificationTemplates
|
||||||
import au.gov.health.covidsafe.preference.Preference
|
import au.gov.health.covidsafe.preference.Preference
|
||||||
import au.gov.health.covidsafe.receivers.PrivacyCleanerReceiver
|
import au.gov.health.covidsafe.sensor.Sensor
|
||||||
import au.gov.health.covidsafe.status.Status
|
import au.gov.health.covidsafe.sensor.SensorArray
|
||||||
import au.gov.health.covidsafe.status.persistence.StatusRecord
|
import au.gov.health.covidsafe.sensor.SensorDelegate
|
||||||
import au.gov.health.covidsafe.status.persistence.StatusRecordStorage
|
import au.gov.health.covidsafe.sensor.ble.BLEDevice
|
||||||
import au.gov.health.covidsafe.streetpass.ConnectionRecord
|
import au.gov.health.covidsafe.sensor.ble.BLESensorConfiguration
|
||||||
import au.gov.health.covidsafe.streetpass.StreetPassScanner
|
import au.gov.health.covidsafe.sensor.ble.BLE_TxPower
|
||||||
import au.gov.health.covidsafe.streetpass.StreetPassServer
|
import au.gov.health.covidsafe.sensor.datatype.*
|
||||||
import au.gov.health.covidsafe.streetpass.StreetPassWorker
|
import au.gov.health.covidsafe.sensor.payload.PayloadDataSupplier
|
||||||
import au.gov.health.covidsafe.streetpass.persistence.Encryption
|
import au.gov.health.covidsafe.streetpass.persistence.Encryption
|
||||||
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord
|
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord
|
||||||
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase
|
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.streetpass.persistence.StreetPassRecordStorage
|
||||||
import au.gov.health.covidsafe.ui.utils.LocalBlobV2
|
import au.gov.health.covidsafe.ui.utils.LocalBlobV2
|
||||||
import au.gov.health.covidsafe.ui.utils.Utils
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import pub.devrel.easypermissions.EasyPermissions
|
import pub.devrel.easypermissions.EasyPermissions
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.HashMap
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
private const val POWER_SAVE_WHITELIST_CHANGED = "android.os.action.POWER_SAVE_WHITELIST_CHANGED"
|
private const val POWER_SAVE_WHITELIST_CHANGED = "android.os.action.POWER_SAVE_WHITELIST_CHANGED"
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
|
class BluetoothMonitoringService : LifecycleService(), CoroutineScope, SensorDelegate, PayloadDataSupplier {
|
||||||
|
|
||||||
@Keep
|
@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 val bluetoothStatusReceiver = BluetoothStatusReceiver()
|
||||||
|
|
||||||
private lateinit var streetPassRecordStorage: StreetPassRecordStorage
|
|
||||||
private lateinit var statusRecordStorage: StatusRecordStorage
|
|
||||||
|
|
||||||
private var job: Job = Job()
|
private var job: Job = Job()
|
||||||
|
|
||||||
override val coroutineContext: CoroutineContext
|
override val coroutineContext: CoroutineContext
|
||||||
|
@ -86,52 +63,42 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
|
||||||
|
|
||||||
private lateinit var commandHandler: CommandHandler
|
private lateinit var commandHandler: CommandHandler
|
||||||
|
|
||||||
private lateinit var localBroadcastManager: LocalBroadcastManager
|
|
||||||
|
|
||||||
private val awsClient = NetworkFactory.awsClient
|
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<String,Date> = HashMap()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
localBroadcastManager = LocalBroadcastManager.getInstance(this)
|
AppContext = applicationContext
|
||||||
setup()
|
setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setup() {
|
private fun setup() {
|
||||||
|
streetPassRecordStorage = StreetPassRecordStorage(applicationContext)
|
||||||
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
|
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
|
|
||||||
CentralLog.setPowerManager(pm)
|
CentralLog.setPowerManager(pm)
|
||||||
|
|
||||||
commandHandler = CommandHandler(WeakReference(this))
|
commandHandler = CommandHandler(WeakReference(this))
|
||||||
|
|
||||||
CentralLog.d(TAG, "Creating service - BluetoothMonitoringService")
|
broadcastMessage = Utils.retrieveBroadcastMessage(this.applicationContext)
|
||||||
serviceUUID = BuildConfig.BLE_SSID
|
|
||||||
|
|
||||||
worker = StreetPassWorker(this.applicationContext)
|
|
||||||
|
|
||||||
unregisterReceivers()
|
unregisterReceivers()
|
||||||
registerReceivers()
|
registerReceivers()
|
||||||
|
|
||||||
streetPassRecordStorage = StreetPassRecordStorage(this.applicationContext)
|
|
||||||
statusRecordStorage = StatusRecordStorage(this.applicationContext)
|
|
||||||
PrivacyCleanerReceiver.startAlarm(this.applicationContext)
|
|
||||||
setupNotifications()
|
setupNotifications()
|
||||||
broadcastMessage = Utils.retrieveBroadcastMessage(this.applicationContext)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun teardown() {
|
fun teardown() {
|
||||||
streetPassServer?.tearDown()
|
|
||||||
streetPassServer = null
|
|
||||||
|
|
||||||
streetPassScanner?.stopScan()
|
|
||||||
streetPassScanner = null
|
|
||||||
|
|
||||||
commandHandler.removeCallbacksAndMessages(null)
|
commandHandler.removeCallbacksAndMessages(null)
|
||||||
|
|
||||||
Utils.cancelBMUpdateCheck(this.applicationContext)
|
Utils.cancelBMUpdateCheck(this.applicationContext)
|
||||||
Utils.cancelNextScan(this.applicationContext)
|
Utils.cancelNextScan(this.applicationContext)
|
||||||
Utils.cancelNextAdvertise(this.applicationContext)
|
Utils.cancelNextAdvertise(this.applicationContext)
|
||||||
|
sensor?.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupNotifications() {
|
private fun setupNotifications() {
|
||||||
|
@ -227,30 +194,17 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
|
||||||
|
|
||||||
CentralLog.i(TAG, "Command is:${cmd?.string}")
|
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) {
|
when (cmd) {
|
||||||
Command.ACTION_START -> {
|
Command.ACTION_START -> {
|
||||||
setupService()
|
|
||||||
actionStart()
|
actionStart()
|
||||||
Utils.scheduleNextHealthCheck(this.applicationContext, healthCheckInterval)
|
Utils.scheduleNextHealthCheck(this.applicationContext, healthCheckInterval)
|
||||||
Utils.scheduleBMUpdateCheck(this.applicationContext, bmCheckInterval)
|
Utils.scheduleBMUpdateCheck(this.applicationContext, bmCheckInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
Command.ACTION_SCAN -> {
|
Command.ACTION_SCAN -> {
|
||||||
actionScan()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Command.ACTION_ADVERTISE -> {
|
Command.ACTION_ADVERTISE -> {
|
||||||
actionAdvertise()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Command.ACTION_UPDATE_BM -> {
|
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() {
|
private fun actionStart() {
|
||||||
if (Preference.isOnBoarded(this)) {
|
if (Preference.isOnBoarded(this)) {
|
||||||
CentralLog.d(TAG, "Service Starting ")
|
CentralLog.d(TAG, "Service Starting ")
|
||||||
|
@ -299,15 +242,45 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
|
||||||
params = null,
|
params = null,
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
broadcastMessage = it.tempId
|
broadcastMessage = it.tempId
|
||||||
setupCycles()
|
sensorStart()
|
||||||
},
|
},
|
||||||
onFailure = {
|
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() {
|
private fun actionUpdateBm() {
|
||||||
|
@ -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() {
|
private fun performHealthCheck() {
|
||||||
|
|
||||||
CentralLog.i(TAG, "Performing self diagnosis")
|
CentralLog.i(TAG, "Performing self diagnosis")
|
||||||
|
@ -443,29 +320,6 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
|
||||||
CHANNEL_ID
|
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() {
|
override fun onDestroy() {
|
||||||
|
@ -475,9 +329,6 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
|
||||||
teardown()
|
teardown()
|
||||||
unregisterReceivers()
|
unregisterReceivers()
|
||||||
|
|
||||||
worker?.terminateConnections()
|
|
||||||
worker?.unregisterReceivers()
|
|
||||||
|
|
||||||
job.cancel()
|
job.cancel()
|
||||||
|
|
||||||
CentralLog.i(TAG, "BluetoothMonitoringService destroyed")
|
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() {
|
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 {
|
try {
|
||||||
unregisterReceiver(bluetoothStatusReceiver)
|
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() {
|
inner class BluetoothStatusReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
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) {
|
enum class Command(val index: Int, val string: String) {
|
||||||
INVALID(-1, "INVALID"),
|
INVALID(-1, "INVALID"),
|
||||||
ACTION_START(0, "START"),
|
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<PayloadData>?, fromTarget: TargetIdentifier?) {
|
||||||
|
val payloads: MutableList<String> = 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<String, Date>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<PayloadData> {
|
||||||
|
// 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<PayloadData> = 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 {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "BTMService"
|
private const val TAG = "BTMService"
|
||||||
|
@ -744,25 +622,22 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
|
||||||
const val PENDING_WIZARD_REQ_CODE = 10
|
const val PENDING_WIZARD_REQ_CODE = 10
|
||||||
const val PENDING_BM_UPDATE = 11
|
const val PENDING_BM_UPDATE = 11
|
||||||
const val PENDING_PRIVACY_CLEANER_CODE = 12
|
const val PENDING_PRIVACY_CLEANER_CODE = 12
|
||||||
const val DAILY_UPLOAD_NOTIFICATION_CODE = 13
|
|
||||||
|
|
||||||
|
|
||||||
var broadcastMessage: String? = null
|
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 maxQueueTime: Long = BuildConfig.MAX_QUEUE_TIME
|
||||||
const val bmCheckInterval: Long = BuildConfig.BM_CHECK_INTERVAL
|
const val bmCheckInterval: Long = BuildConfig.BM_CHECK_INTERVAL
|
||||||
const val healthCheckInterval: Long = BuildConfig.HEALTH_CHECK_INTERVAL
|
const val healthCheckInterval: Long = BuildConfig.HEALTH_CHECK_INTERVAL
|
||||||
|
|
||||||
const val connectionTimeout: Long = BuildConfig.CONNECTION_TIMEOUT
|
const val connectionTimeout: Long = BuildConfig.CONNECTION_TIMEOUT
|
||||||
|
|
||||||
const val blacklistDuration: Long = BuildConfig.BLACKLIST_DURATION
|
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 ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import java.lang.reflect.InvocationHandler;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import au.gov.health.covidsafe.logging.CentralLog;
|
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
|
* IBluetoothGatt implementation (the interface between Android SDK's bluetooth API and the system
|
||||||
* bluetooth daemon)
|
* bluetooth daemon)
|
||||||
|
|
||||||
|
|
|
@ -149,7 +149,13 @@ object StreetPassPairingFix {
|
||||||
// Instance - this is what is called to initiate a read/write to a preipheral
|
// Instance - this is what is called to initiate a read/write to a preipheral
|
||||||
val mService: Object = mServiceField!!.get(gatt) as Object
|
val mService: Object = mServiceField!!.get(gatt) as Object
|
||||||
|
|
||||||
// Wrap the IBLuetoothGatt instance in a Proxy object in order to intercept calls to
|
// 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
|
// readCharacteristic and writeCharacteristic. IBluetoothGattInvocationHandler will catch
|
||||||
// calls to these functions and rewrite their authReq field to ensure no pairing attempts
|
// calls to these functions and rewrite their authReq field to ensure no pairing attempts
|
||||||
// occur
|
// occur
|
||||||
|
@ -159,7 +165,7 @@ object StreetPassPairingFix {
|
||||||
|
|
||||||
// Write the proxy back to BluetoothGatt.mService
|
// Write the proxy back to BluetoothGatt.mService
|
||||||
mServiceField!!.set(gatt, mServiceProxy)
|
mServiceField!!.set(gatt, mServiceProxy)
|
||||||
|
}
|
||||||
// Reset accessibility
|
// Reset accessibility
|
||||||
mServiceField!!.isAccessible = mServiceAccessible
|
mServiceField!!.isAccessible = mServiceAccessible
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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(
|
override fun onCharacteristicWrite(
|
||||||
gatt: BluetoothGatt,
|
gatt: BluetoothGatt,
|
||||||
|
|
|
@ -25,5 +25,4 @@ class StreetPassRecordStorage(val context: Context) {
|
||||||
fun getAllRecords(): List<StreetPassRecord> {
|
fun getAllRecords(): List<StreetPassRecord> {
|
||||||
return recordDao.getCurrentRecords()
|
return recordDao.getCurrentRecords()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -16,7 +16,6 @@ import android.view.View.GONE
|
||||||
import android.view.View.VISIBLE
|
import android.view.View.VISIBLE
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
import androidx.constraintlayout.solver.GoalRow
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
@ -100,10 +99,15 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks, Networ
|
||||||
initializeRefreshButton()
|
initializeRefreshButton()
|
||||||
initializePullToRefresh()
|
initializePullToRefresh()
|
||||||
initialiseReRegistration()
|
initialiseReRegistration()
|
||||||
|
setupHyperlink()
|
||||||
|
|
||||||
NetworkConnectionCheck.addNetworkChangedListener(requireContext(), this)
|
NetworkConnectionCheck.addNetworkChangedListener(requireContext(), this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupHyperlink() {
|
||||||
|
txt_update_description.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
private fun initializeNoNetworkError() {
|
private fun initializeNoNetworkError() {
|
||||||
no_network_error_text_view.setOnClickListener {
|
no_network_error_text_view.setOnClickListener {
|
||||||
startActivity(Intent(android.provider.Settings.ACTION_WIRELESS_SETTINGS))
|
startActivity(Intent(android.provider.Settings.ACTION_WIRELESS_SETTINGS))
|
||||||
|
@ -235,6 +239,10 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks, Networ
|
||||||
homeFragmentViewModel.collectionMessageVisible.value = false
|
homeFragmentViewModel.collectionMessageVisible.value = false
|
||||||
askForLocationPermission()
|
askForLocationPermission()
|
||||||
}
|
}
|
||||||
|
btn_proceed.setOnClickListener {
|
||||||
|
layout_herald_upgrade.slideAnimation(SlideDirection.UP, SlideType.HIDE, 200)
|
||||||
|
homeFragmentViewModel.heraldUpgradeMessage.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeUploadTestDataNavigation() {
|
private fun initializeUploadTestDataNavigation() {
|
||||||
|
|
|
@ -23,6 +23,7 @@ class HomeFragmentViewModel(application: Application) : AndroidViewModel(applica
|
||||||
val caseStatisticsLiveData = MutableLiveData<CaseStatisticResponse>()
|
val caseStatisticsLiveData = MutableLiveData<CaseStatisticResponse>()
|
||||||
val isRefreshing = MutableLiveData<Boolean>()
|
val isRefreshing = MutableLiveData<Boolean>()
|
||||||
val collectionMessageVisible = MutableLiveData<Boolean>()
|
val collectionMessageVisible = MutableLiveData<Boolean>()
|
||||||
|
val heraldUpgradeMessage = MutableLiveData<Boolean>()
|
||||||
// Show = true and hide = false
|
// Show = true and hide = false
|
||||||
val turnCaseNumber = MutableLiveData<Boolean>()
|
val turnCaseNumber = MutableLiveData<Boolean>()
|
||||||
lateinit var context: Context
|
lateinit var context: Context
|
||||||
|
@ -72,10 +73,13 @@ class HomeFragmentViewModel(application: Application) : AndroidViewModel(applica
|
||||||
val latestVersion = Preference.getBuildNumber(context)
|
val latestVersion = Preference.getBuildNumber(context)
|
||||||
// When We want to show disclaimer to user after update, minVersionShowPolicy should be as same as the current version
|
// When We want to show disclaimer to user after update, minVersionShowPolicy should be as same as the current version
|
||||||
val minVersionShowPolicy = 74
|
val minVersionShowPolicy = 74
|
||||||
|
val minVersionHeraldPolicy = 94
|
||||||
val currentVersion = BuildConfig.VERSION_CODE
|
val currentVersion = BuildConfig.VERSION_CODE
|
||||||
if (latestVersion == 0) {
|
if (latestVersion == 0) {
|
||||||
collectionMessageVisible.value = true
|
collectionMessageVisible.value = true
|
||||||
|
heraldUpgradeMessage.value = true
|
||||||
} else {
|
} else {
|
||||||
|
heraldUpgradeMessage.value = currentVersion <= minVersionHeraldPolicy && currentVersion > latestVersion
|
||||||
collectionMessageVisible.value = currentVersion <= minVersionShowPolicy && currentVersion > latestVersion
|
collectionMessageVisible.value = currentVersion <= minVersionShowPolicy && currentVersion > latestVersion
|
||||||
}
|
}
|
||||||
Preference.putBuildNumber(context, currentVersion)
|
Preference.putBuildNumber(context, currentVersion)
|
||||||
|
|
|
@ -12,27 +12,25 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import au.gov.health.covidsafe.HomeActivity
|
import au.gov.health.covidsafe.HomeActivity
|
||||||
import au.gov.health.covidsafe.preference.Preference
|
|
||||||
import au.gov.health.covidsafe.R
|
import au.gov.health.covidsafe.R
|
||||||
import au.gov.health.covidsafe.app.TracerApp
|
import au.gov.health.covidsafe.app.TracerApp
|
||||||
import au.gov.health.covidsafe.extensions.*
|
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.talkback.setHeading
|
||||||
import au.gov.health.covidsafe.ui.base.PagerChildFragment
|
import au.gov.health.covidsafe.ui.base.PagerChildFragment
|
||||||
import au.gov.health.covidsafe.ui.base.UploadButtonLayout
|
import au.gov.health.covidsafe.ui.base.UploadButtonLayout
|
||||||
import kotlinx.android.synthetic.main.fragment_permission.*
|
import kotlinx.android.synthetic.main.fragment_permission.*
|
||||||
import pub.devrel.easypermissions.EasyPermissions
|
import pub.devrel.easypermissions.EasyPermissions
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class PermissionFragment : PagerChildFragment(), EasyPermissions.PermissionCallbacks {
|
class PermissionFragment : PagerChildFragment(), EasyPermissions.PermissionCallbacks {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
val requiredPermissions = PermissionFragment().requestPermissions()
|
||||||
val requiredPermissions = arrayOf(
|
|
||||||
Manifest.permission.BLUETOOTH,
|
|
||||||
Manifest.permission.BLUETOOTH_ADMIN,
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override var step: Int? = 4
|
override var step: Int? = 4
|
||||||
|
@ -72,6 +70,20 @@ class PermissionFragment : PagerChildFragment(), EasyPermissions.PermissionCallb
|
||||||
} else super.onActivityResult(requestCode, resultCode, data)
|
} else super.onActivityResult(requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun requestPermissions(): MutableList<String> {
|
||||||
|
// Check and request permissions
|
||||||
|
val requiredPermissions: MutableList<String> = 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() {
|
private fun navigateToNextPage() {
|
||||||
navigationStarted = false
|
navigationStarted = false
|
||||||
if (hasAllPermissionsAndBluetoothOn()) {
|
if (hasAllPermissionsAndBluetoothOn()) {
|
||||||
|
|
|
@ -124,7 +124,8 @@
|
||||||
app:layout_anchorGravity="center"
|
app:layout_anchorGravity="center"
|
||||||
android:background="@color/white"
|
android:background="@color/white"
|
||||||
android:elevation="12dp"
|
android:elevation="12dp"
|
||||||
visibility="@{viewModel.collectionMessageVisible}">
|
visibility="@{viewModel.collectionMessageVisible}"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -178,6 +179,85 @@
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/layout_herald_upgrade"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_anchor="@+id/swipeRefreshLayout"
|
||||||
|
app:layout_anchorGravity="center"
|
||||||
|
android:background="@color/white"
|
||||||
|
android:elevation="12dp"
|
||||||
|
visibility="@{viewModel.heraldUpgradeMessage}">
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingStart="@dimen/space_32"
|
||||||
|
android:paddingEnd="@dimen/space_32"
|
||||||
|
android:paddingTop="@dimen/space_40"
|
||||||
|
android:scrollbarStyle="outsideInset"
|
||||||
|
android:scrollbars="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/permission_img"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_permission"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/txt_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/space_8"
|
||||||
|
android:text="@string/update_heading_android"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textAppearance="?textAppearanceHeadline1"
|
||||||
|
android:textSize="28sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/permission_img" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/txt_update_description"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/space_16"
|
||||||
|
android:layout_marginBottom="@dimen/space_16"
|
||||||
|
android:text="@string/update_description_android"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
style="?textAppearanceBody1"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:lineHeight="@dimen/space_24"
|
||||||
|
android:linksClickable="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/txt_title" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_proceed"
|
||||||
|
style="?attr/textAppearanceButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/space_40"
|
||||||
|
android:layout_marginBottom="@dimen/space_32"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/permission_button"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/txt_update_description"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</FrameLayout>
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
</layout>
|
</layout>
|
|
@ -242,7 +242,6 @@
|
||||||
<string name="data_privacy_headline">التسجيل والخصوصية</string>
|
<string name="data_privacy_headline">التسجيل والخصوصية</string>
|
||||||
<string name="data_privacy_headline_content_description">العنوان والتسجيل والخصوصية</string>
|
<string name="data_privacy_headline_content_description">العنوان والتسجيل والخصوصية</string>
|
||||||
<string name="deaths">الوفيات</string>
|
<string name="deaths">الوفيات</string>
|
||||||
<string name="dialog_error_uploading_message">حدث خطأ أثناء تحميل معلوماتك، يرجى المحاولة مرة أخرى.</string>
|
|
||||||
<string name="dialog_error_uploading_negative">إلغاء</string>
|
<string name="dialog_error_uploading_negative">إلغاء</string>
|
||||||
<string name="dialog_error_uploading_positive">حاول مرة أخرى</string>
|
<string name="dialog_error_uploading_positive">حاول مرة أخرى</string>
|
||||||
<string name="dialog_uploading_message">يتم الآن تحميل معلومات COVIDSafe الخاصة بك. \n\n الرجاء عدم إغلاق التطبيق.</string>
|
<string name="dialog_uploading_message">يتم الآن تحميل معلومات COVIDSafe الخاصة بك. \n\n الرجاء عدم إغلاق التطبيق.</string>
|
||||||
|
|
|
@ -242,7 +242,6 @@
|
||||||
<string name="data_privacy_headline">Εγγραφή και προστασία του απορρήτου</string>
|
<string name="data_privacy_headline">Εγγραφή και προστασία του απορρήτου</string>
|
||||||
<string name="data_privacy_headline_content_description">Επικεφαλίδα, Εγγραφή και προστασία του απορρήτου</string>
|
<string name="data_privacy_headline_content_description">Επικεφαλίδα, Εγγραφή και προστασία του απορρήτου</string>
|
||||||
<string name="deaths">Θάνατοι</string>
|
<string name="deaths">Θάνατοι</string>
|
||||||
<string name="dialog_error_uploading_message">Παρουσιάστηκε πρόβλημα κατά την ανάρτηση των στοιχείων σας. Δοκιμάστε ξανά.</string>
|
|
||||||
<string name="dialog_error_uploading_negative">Ακύρωση</string>
|
<string name="dialog_error_uploading_negative">Ακύρωση</string>
|
||||||
<string name="dialog_error_uploading_positive">Προσπαθήστε ξανά</string>
|
<string name="dialog_error_uploading_positive">Προσπαθήστε ξανά</string>
|
||||||
<string name="dialog_uploading_message">Τα στοιχεία σας αναρτίζονται στην COVIDSafe αυτή τη στιγμή. \n\nΜην κλείσετε την εφαρμογή.</string>
|
<string name="dialog_uploading_message">Τα στοιχεία σας αναρτίζονται στην COVIDSafe αυτή τη στιγμή. \n\nΜην κλείσετε την εφαρμογή.</string>
|
||||||
|
|
|
@ -242,7 +242,6 @@
|
||||||
<string name="data_privacy_headline">Registrazione e privacy</string>
|
<string name="data_privacy_headline">Registrazione e privacy</string>
|
||||||
<string name="data_privacy_headline_content_description">Titolo, Registrazione e privacy</string>
|
<string name="data_privacy_headline_content_description">Titolo, Registrazione e privacy</string>
|
||||||
<string name="deaths">Decessi</string>
|
<string name="deaths">Decessi</string>
|
||||||
<string name="dialog_error_uploading_message">Si è verificato un errore durante il caricamento delle informazioni. Riprova.</string>
|
|
||||||
<string name="dialog_error_uploading_negative">Annulla</string>
|
<string name="dialog_error_uploading_negative">Annulla</string>
|
||||||
<string name="dialog_error_uploading_positive">Riprova</string>
|
<string name="dialog_error_uploading_positive">Riprova</string>
|
||||||
<string name="dialog_uploading_message">I dati di COVIDSafe sono attualmente in fase di caricamento. \n\nNon chiudere l\'app.</string>
|
<string name="dialog_uploading_message">I dati di COVIDSafe sono attualmente in fase di caricamento. \n\nNon chiudere l\'app.</string>
|
||||||
|
|
|
@ -242,7 +242,6 @@
|
||||||
<string name="data_privacy_headline">등록 및 개인정보 보호</string>
|
<string name="data_privacy_headline">등록 및 개인정보 보호</string>
|
||||||
<string name="data_privacy_headline_content_description">제목, 등록 및 개인정보 보호</string>
|
<string name="data_privacy_headline_content_description">제목, 등록 및 개인정보 보호</string>
|
||||||
<string name="deaths">사망자</string>
|
<string name="deaths">사망자</string>
|
||||||
<string name="dialog_error_uploading_message">정보를 업로드하는 동안 오류가 발생했습니다. 다시 시도하세요.</string>
|
|
||||||
<string name="dialog_error_uploading_negative">취소</string>
|
<string name="dialog_error_uploading_negative">취소</string>
|
||||||
<string name="dialog_error_uploading_positive">다시 시도</string>
|
<string name="dialog_error_uploading_positive">다시 시도</string>
|
||||||
<string name="dialog_uploading_message">당신의 COVIDSafe 정보가 현재 업로드 중입니다. \n\n앱을 닫지 마세요.</string>
|
<string name="dialog_uploading_message">당신의 COVIDSafe 정보가 현재 업로드 중입니다. \n\n앱을 닫지 마세요.</string>
|
||||||
|
|
|
@ -242,7 +242,6 @@
|
||||||
<string name="data_privacy_headline">ਪੰਜੀਕਰਨ ਅਤੇ ਪ੍ਰਾਈਵੇਸੀ</string>
|
<string name="data_privacy_headline">ਪੰਜੀਕਰਨ ਅਤੇ ਪ੍ਰਾਈਵੇਸੀ</string>
|
||||||
<string name="data_privacy_headline_content_description">ਸਿਰਲੇਖ, ਪੰਜੀਕਰਨ ਅਤੇ ਪ੍ਰਾਈਵੇਸੀ</string>
|
<string name="data_privacy_headline_content_description">ਸਿਰਲੇਖ, ਪੰਜੀਕਰਨ ਅਤੇ ਪ੍ਰਾਈਵੇਸੀ</string>
|
||||||
<string name="deaths">ਮੌਤਾਂ</string>
|
<string name="deaths">ਮੌਤਾਂ</string>
|
||||||
<string name="dialog_error_uploading_message">ਤੁਹਾਡੀ ਜਾਣਕਾਰੀ ਨੂੰ ਅੱਪਲੋਡ ਕਰਨ ਦੌਰਾਨ ਇੱਕ ਗਲਤੀ ਆਈ, ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।</string>
|
|
||||||
<string name="dialog_error_uploading_negative">ਰੱਦ</string>
|
<string name="dialog_error_uploading_negative">ਰੱਦ</string>
|
||||||
<string name="dialog_error_uploading_positive">ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ</string>
|
<string name="dialog_error_uploading_positive">ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ</string>
|
||||||
<string name="dialog_uploading_message">ਤੁਹਾਡੀ COVIDSafe ਜਾਣਕਾਰੀ ਨੂੰ ਇਸ ਵੇਲੇ ਅੱਪਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ।\n\nਕਿਰਪਾ ਕਰਕੇ ਐਪ ਬੰਦ ਨਾ ਕਰੋ।</string>
|
<string name="dialog_uploading_message">ਤੁਹਾਡੀ COVIDSafe ਜਾਣਕਾਰੀ ਨੂੰ ਇਸ ਵੇਲੇ ਅੱਪਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ।\n\nਕਿਰਪਾ ਕਰਕੇ ਐਪ ਬੰਦ ਨਾ ਕਰੋ।</string>
|
||||||
|
@ -347,6 +346,7 @@
|
||||||
<string name="IssueFooter">ਤੁਹਾਡੇ ਫੀਡਬੈਕ ਬਾਰੇ ਹੋਰ ਵਿਸਥਾਰਾਂ ਲਈ ਅਸੀਂ ਤੁਹਾਡੇ ਨਾਲ ਸੰਪਰਕ ਕਰ ਸਕਦੇ ਹਾਂ| ਤੁਹਾਡੀ ਈਮੇਲ ਦੀ ਵਰਤੋਂ ਕਿਸੇ ਹੋਰ ਉਦੇਸ਼ ਲਈ ਨਹੀਂ ਕੀਤੀ ਜਾਵੇਗੀ|</string>
|
<string name="IssueFooter">ਤੁਹਾਡੇ ਫੀਡਬੈਕ ਬਾਰੇ ਹੋਰ ਵਿਸਥਾਰਾਂ ਲਈ ਅਸੀਂ ਤੁਹਾਡੇ ਨਾਲ ਸੰਪਰਕ ਕਰ ਸਕਦੇ ਹਾਂ| ਤੁਹਾਡੀ ਈਮੇਲ ਦੀ ਵਰਤੋਂ ਕਿਸੇ ਹੋਰ ਉਦੇਸ਼ ਲਈ ਨਹੀਂ ਕੀਤੀ ਜਾਵੇਗੀ|</string>
|
||||||
<string name="jwt_description">ਤੁਹਾਡੇ ਪੰਜੀਕਰਨ (ਰਜ਼ਿਸਟ੍ਰੇਸ਼ਨ) ਦੇ ਵਿਸਥਾਰਾਂ ਵਿੱਚ ਕੋਈ ਸਮੱਸਿਆ ਹੈ।</string>
|
<string name="jwt_description">ਤੁਹਾਡੇ ਪੰਜੀਕਰਨ (ਰਜ਼ਿਸਟ੍ਰੇਸ਼ਨ) ਦੇ ਵਿਸਥਾਰਾਂ ਵਿੱਚ ਕੋਈ ਸਮੱਸਿਆ ਹੈ।</string>
|
||||||
<string name="jwt_heading">ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਰਜਿਸਟਰ ਕਰੋ</string>
|
<string name="jwt_heading">ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਰਜਿਸਟਰ ਕਰੋ</string>
|
||||||
|
<string name="jwt_success">ਰਜਿਸਟਰੇਸ਼ਨ ਸਫਲਤਾਪੂਰਵਕ ਰਿਨਿਯੂ ਕੀਤਾ ਗਿਆ</string>
|
||||||
<string name="loading_numbers">ਤਾਜ਼ਾ ਅੰਕੜੇ ਲੋਡ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ</string>
|
<string name="loading_numbers">ਤਾਜ਼ਾ ਅੰਕੜੇ ਲੋਡ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ</string>
|
||||||
<!-- Splash Screen -->
|
<!-- Splash Screen -->
|
||||||
<string name="migration_in_progress">COVIDSafe ਅੱਪਡੇਟ ਚੱਲ ਰਿਹਾ ਹੈ। \n\nਕਿਰਪਾ ਕਰਕੇ ਯਕੀਨੀ ਬਣਾਓ ਕਿ ਤੁਹਾਡਾ ਫ਼ੋਨ ਤਦ ਤੱਕ ਬੰਦ ਨਹੀਂ ਹੋਣਾ ਚਾਹੀਦਾ ਜਦ ਤੱਕ ਅੱਪਡੇਟ ਪੂਰਾ ਨਹੀਂ ਹੋ ਜਾਂਦਾ।</string>
|
<string name="migration_in_progress">COVIDSafe ਅੱਪਡੇਟ ਚੱਲ ਰਿਹਾ ਹੈ। \n\nਕਿਰਪਾ ਕਰਕੇ ਯਕੀਨੀ ਬਣਾਓ ਕਿ ਤੁਹਾਡਾ ਫ਼ੋਨ ਤਦ ਤੱਕ ਬੰਦ ਨਹੀਂ ਹੋਣਾ ਚਾਹੀਦਾ ਜਦ ਤੱਕ ਅੱਪਡੇਟ ਪੂਰਾ ਨਹੀਂ ਹੋ ਜਾਂਦਾ।</string>
|
||||||
|
|
|
@ -242,7 +242,6 @@
|
||||||
<string name="data_privacy_headline">Kayıt ve gizlilik</string>
|
<string name="data_privacy_headline">Kayıt ve gizlilik</string>
|
||||||
<string name="data_privacy_headline_content_description">Başlık, Kayıt ve gizlilik</string>
|
<string name="data_privacy_headline_content_description">Başlık, Kayıt ve gizlilik</string>
|
||||||
<string name="deaths">Ölümler</string>
|
<string name="deaths">Ölümler</string>
|
||||||
<string name="dialog_error_uploading_message">Bilgileriniz sisteme yüklerken bir hata oluştu, lütfen tekrar deneyin.</string>
|
|
||||||
<string name="dialog_error_uploading_negative">İptal</string>
|
<string name="dialog_error_uploading_negative">İptal</string>
|
||||||
<string name="dialog_error_uploading_positive">Tekrar deneyin</string>
|
<string name="dialog_error_uploading_positive">Tekrar deneyin</string>
|
||||||
<string name="dialog_uploading_message">COVIDSafe bilgileriniz şu anda sisteme yükleniyor. \n\n Lütfen uygulamayı kapatmayın.</string>
|
<string name="dialog_uploading_message">COVIDSafe bilgileriniz şu anda sisteme yükleniyor. \n\n Lütfen uygulamayı kapatmayın.</string>
|
||||||
|
|
|
@ -242,7 +242,6 @@
|
||||||
<string name="data_privacy_headline">Đăng ký và bảo mật</string>
|
<string name="data_privacy_headline">Đăng ký và bảo mật</string>
|
||||||
<string name="data_privacy_headline_content_description">Tiêu đề, Đăng ký và bảo mật</string>
|
<string name="data_privacy_headline_content_description">Tiêu đề, Đăng ký và bảo mật</string>
|
||||||
<string name="deaths">Tử vong</string>
|
<string name="deaths">Tử vong</string>
|
||||||
<string name="dialog_error_uploading_message">Bị lỗi trong khi đăng tải thông tin của bạn, vui lòng thử lại.</string>
|
|
||||||
<string name="dialog_error_uploading_negative">Hủy bỏ</string>
|
<string name="dialog_error_uploading_negative">Hủy bỏ</string>
|
||||||
<string name="dialog_error_uploading_positive">Thử lại</string>
|
<string name="dialog_error_uploading_positive">Thử lại</string>
|
||||||
<string name="dialog_uploading_message">Thông tin COVIDSafe của bạn hiện đang được đăng tải. \n\n Vui lòng không đóng ứng dụng.</string>
|
<string name="dialog_uploading_message">Thông tin COVIDSafe của bạn hiện đang được đăng tải. \n\n Vui lòng không đóng ứng dụng.</string>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue