mirror of
https://github.com/AU-COVIDSafe/mobile-android.git
synced 2025-01-18 16:56:34 +00:00
Herald debug code for tech community
This commit is contained in:
parent
6bf46ded07
commit
2ef00979e5
105 changed files with 6184 additions and 975 deletions
11
README.md
11
README.md
|
@ -1,3 +1,14 @@
|
||||||
|
# The new Herald Protocol is being integrated into COVIDSafe
|
||||||
|
|
||||||
|
Even though we have made a range of significant improvements to COVIDSafe, we continue to look for ways we can improve the app further. Herald, a Bluetooth communication and range finding protocol that supports contact tracing, is one such improvement.
|
||||||
|
|
||||||
|
The Herald Protocol employs several techniques to improve Bluetooth communication across a wide range of mobile devices. This provides contact tracing apps with regular and accurate proximity information that helps make them highly effective, especially in background on iOS devices.
|
||||||
|
|
||||||
|
Herald is a VMware-originated open source project. It is part of VMware’s ongoing contribution towards the Linux Foundation Public Health initiative. The initiative aims to use open source technologies to help public health authorities across the world combat COVID-19.
|
||||||
|
|
||||||
|
Find out more about Herald and COVIDSafe: [https://www.dta.gov.au/news/covidsafe-captures-close-contacts-new-herald-protocol](https://www.dta.gov.au/news/covidsafe-captures-close-contacts-new-herald-protocol)
|
||||||
|
|
||||||
|
|
||||||
# COVIDSafe app
|
# COVIDSafe app
|
||||||
|
|
||||||
Thank you for viewing the GitHub repository for the COVIDSafe app by the Australian Government.
|
Thank you for viewing the GitHub repository for the COVIDSafe app by the Australian Government.
|
||||||
|
|
|
@ -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 89
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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,35 @@ 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 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? {
|
||||||
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 +42,5 @@ class TracerApp : Application() {
|
||||||
fun asCentralDevice(): CentralDevice {
|
fun asCentralDevice(): CentralDevice {
|
||||||
return CentralDevice(Build.MODEL, "SELF")
|
return CentralDevice(Build.MODEL, "SELF")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,306 +0,0 @@
|
||||||
package au.gov.health.covidsafe.bluetooth.gatt
|
|
||||||
|
|
||||||
import android.bluetooth.*
|
|
||||||
import android.bluetooth.BluetoothGatt.GATT_FAILURE
|
|
||||||
import android.bluetooth.BluetoothGatt.GATT_SUCCESS
|
|
||||||
import android.content.Context
|
|
||||||
import au.gov.health.covidsafe.app.TracerApp
|
|
||||||
import au.gov.health.covidsafe.ui.utils.Utils
|
|
||||||
import au.gov.health.covidsafe.logging.CentralLog
|
|
||||||
import au.gov.health.covidsafe.streetpass.CentralDevice
|
|
||||||
import au.gov.health.covidsafe.streetpass.ConnectionRecord
|
|
||||||
import au.gov.health.covidsafe.streetpass.persistence.Encryption
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.properties.Delegates
|
|
||||||
|
|
||||||
class GattServer constructor(val context: Context, serviceUUIDString: String) {
|
|
||||||
|
|
||||||
private val TAG = "GattServer"
|
|
||||||
private var bluetoothManager: BluetoothManager by Delegates.notNull()
|
|
||||||
|
|
||||||
private var serviceUUID: UUID by Delegates.notNull()
|
|
||||||
var bluetoothGattServer: BluetoothGattServer? = null
|
|
||||||
|
|
||||||
val gson: Gson = GsonBuilder().disableHtmlEscaping().create()
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
|
||||||
bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
|
||||||
this.serviceUUID = UUID.fromString(serviceUUIDString)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val gattServerCallback = object : BluetoothGattServerCallback() {
|
|
||||||
|
|
||||||
val writeDataPayload: MutableMap<String, ByteArray> = HashMap()
|
|
||||||
val readPayloadMap: MutableMap<String, ByteArray> = HashMap()
|
|
||||||
|
|
||||||
override fun onConnectionStateChange(device: BluetoothDevice?, status: Int, newState: Int) {
|
|
||||||
when (newState) {
|
|
||||||
BluetoothProfile.STATE_CONNECTED -> {
|
|
||||||
CentralLog.i(TAG, "${device?.address} Connected to local GATT server")
|
|
||||||
device?.let {
|
|
||||||
bluetoothManager.getConnectedDevices(BluetoothProfile.GATT).contains(device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothProfile.STATE_DISCONNECTED -> {
|
|
||||||
CentralLog.i(TAG, "${device?.address} Disconnected from local GATT server.")
|
|
||||||
readPayloadMap.remove(device?.address)
|
|
||||||
device?.let {
|
|
||||||
Utils.broadcastDeviceDisconnected(context, device)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
CentralLog.i(TAG, "Connection status: $newState - ${device?.address}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCharacteristicReadRequest(
|
|
||||||
device: BluetoothDevice?,
|
|
||||||
requestId: Int,
|
|
||||||
offset: Int,
|
|
||||||
characteristic: BluetoothGattCharacteristic?
|
|
||||||
) {
|
|
||||||
|
|
||||||
device?.let {
|
|
||||||
|
|
||||||
CentralLog.i(TAG, "onCharacteristicReadRequest from ${device.address}")
|
|
||||||
|
|
||||||
if (serviceUUID == characteristic?.uuid) {
|
|
||||||
|
|
||||||
if (Utils.bmValid(context)) {
|
|
||||||
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)
|
|
||||||
|
|
||||||
CentralLog.i(
|
|
||||||
TAG,
|
|
||||||
"onCharacteristicReadRequest from ${device.address} - $requestId- $offset - ${String(
|
|
||||||
value,
|
|
||||||
Charsets.UTF_8
|
|
||||||
)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
bluetoothGattServer?.sendResponse(device, requestId, GATT_SUCCESS, 0, value)
|
|
||||||
} else {
|
|
||||||
CentralLog.i(
|
|
||||||
TAG,
|
|
||||||
"onCharacteristicReadRequest from ${device.address} - $requestId- $offset - BM Expired"
|
|
||||||
)
|
|
||||||
bluetoothGattServer?.sendResponse(
|
|
||||||
device,
|
|
||||||
requestId,
|
|
||||||
GATT_FAILURE,
|
|
||||||
0,
|
|
||||||
ByteArray(0)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
CentralLog.i(TAG, "incorrect serviceUUID from ${device.address}")
|
|
||||||
bluetoothGattServer?.sendResponse(device, requestId, GATT_SUCCESS, 0, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device == null) {
|
|
||||||
CentralLog.i(TAG, "No device")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ReadRequestEncryptedPayload(val timestamp: Long, val modelP: String, val msg: String)
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCharacteristicWriteRequest(
|
|
||||||
device: BluetoothDevice?,
|
|
||||||
requestId: Int,
|
|
||||||
characteristic: BluetoothGattCharacteristic,
|
|
||||||
preparedWrite: Boolean,
|
|
||||||
responseNeeded: Boolean,
|
|
||||||
offset: Int,
|
|
||||||
value: ByteArray?
|
|
||||||
) {
|
|
||||||
|
|
||||||
|
|
||||||
device?.let {
|
|
||||||
CentralLog.i(
|
|
||||||
TAG,
|
|
||||||
"onCharacteristicWriteRequest - ${device.address} - preparedWrite: $preparedWrite"
|
|
||||||
)
|
|
||||||
|
|
||||||
CentralLog.i(
|
|
||||||
TAG,
|
|
||||||
"onCharacteristicWriteRequest from ${device.address} - $requestId - $offset"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (serviceUUID == characteristic.uuid) {
|
|
||||||
var valuePassed = ""
|
|
||||||
value?.let {
|
|
||||||
valuePassed = String(value, Charsets.UTF_8)
|
|
||||||
}
|
|
||||||
CentralLog.i(
|
|
||||||
TAG,
|
|
||||||
"onCharacteristicWriteRequest from ${device.address} - $valuePassed"
|
|
||||||
)
|
|
||||||
if (value != null) {
|
|
||||||
var dataBuffer = writeDataPayload[device.address]
|
|
||||||
|
|
||||||
if (dataBuffer == null) {
|
|
||||||
dataBuffer = ByteArray(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
dataBuffer = dataBuffer.plus(value)
|
|
||||||
writeDataPayload[device.address] = dataBuffer
|
|
||||||
|
|
||||||
CentralLog.i(
|
|
||||||
TAG,
|
|
||||||
"Accumulated characteristic: ${String(
|
|
||||||
dataBuffer,
|
|
||||||
Charsets.UTF_8
|
|
||||||
)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (responseNeeded) {
|
|
||||||
CentralLog.i(TAG, "Sending response offset: ${dataBuffer.size}")
|
|
||||||
bluetoothGattServer?.sendResponse(
|
|
||||||
device,
|
|
||||||
requestId,
|
|
||||||
GATT_SUCCESS,
|
|
||||||
dataBuffer.size,
|
|
||||||
value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
CentralLog.i(TAG, "no data from ${device.address}")
|
|
||||||
bluetoothGattServer?.sendResponse(device, requestId, GATT_SUCCESS, 0, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!preparedWrite) {
|
|
||||||
CentralLog.i(
|
|
||||||
TAG,
|
|
||||||
"onCharacteristicWriteRequest - ${device.address} - preparedWrite: $preparedWrite"
|
|
||||||
)
|
|
||||||
|
|
||||||
saveDataSaved(device)
|
|
||||||
bluetoothGattServer?.sendResponse(device, requestId, GATT_SUCCESS, 0, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device == null) {
|
|
||||||
CentralLog.e(TAG, "Write stopped - no device")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onExecuteWrite(device: BluetoothDevice, requestId: Int, execute: Boolean) {
|
|
||||||
super.onExecuteWrite(device, requestId, execute)
|
|
||||||
val data = writeDataPayload[device.address]
|
|
||||||
|
|
||||||
data.let { dataBuffer ->
|
|
||||||
|
|
||||||
if (dataBuffer != null) {
|
|
||||||
CentralLog.i(
|
|
||||||
TAG,
|
|
||||||
"onExecuteWrite - $requestId- ${device.address} - ${String(
|
|
||||||
dataBuffer,
|
|
||||||
Charsets.UTF_8
|
|
||||||
)}"
|
|
||||||
)
|
|
||||||
saveDataSaved(device)
|
|
||||||
bluetoothGattServer?.sendResponse(device, requestId, GATT_SUCCESS, 0, null)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
bluetoothGattServer?.sendResponse(device, requestId, GATT_FAILURE, 0, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveDataSaved(device: BluetoothDevice) {
|
|
||||||
val data = writeDataPayload[device.address]
|
|
||||||
|
|
||||||
data?.let {
|
|
||||||
try {
|
|
||||||
val dataWritten = WriteRequestPayload.createReadRequestPayload(data)
|
|
||||||
device.let {
|
|
||||||
val centralDevice: CentralDevice?
|
|
||||||
|
|
||||||
try {
|
|
||||||
centralDevice = CentralDevice(dataWritten.modelC, device.address)
|
|
||||||
val connectionRecord = ConnectionRecord(
|
|
||||||
version = dataWritten.v,
|
|
||||||
msg = dataWritten.msg,
|
|
||||||
org = dataWritten.org,
|
|
||||||
peripheral = TracerApp.asPeripheralDevice(),
|
|
||||||
central = centralDevice,
|
|
||||||
rssi = dataWritten.rssi,
|
|
||||||
txPower = dataWritten.txPower
|
|
||||||
)
|
|
||||||
|
|
||||||
Utils.broadcastStreetPassReceived(
|
|
||||||
context,
|
|
||||||
connectionRecord
|
|
||||||
)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
CentralLog.e(TAG, "caught error here ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
CentralLog.e(TAG, "Failed to save write payload - ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils.broadcastDeviceProcessed(context, device.address)
|
|
||||||
writeDataPayload.remove(device.address)
|
|
||||||
readPayloadMap.remove(device.address)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startServer(): Boolean {
|
|
||||||
|
|
||||||
bluetoothGattServer = bluetoothManager.openGattServer(context, gattServerCallback)
|
|
||||||
|
|
||||||
bluetoothGattServer?.let {
|
|
||||||
it.clearServices()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addService(service: GattService) {
|
|
||||||
bluetoothGattServer?.addService(service.gattService)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
try {
|
|
||||||
bluetoothGattServer?.clearServices()
|
|
||||||
bluetoothGattServer?.close()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
CentralLog.e(TAG, "GATT server can't be closed elegantly ${e.localizedMessage}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
package au.gov.health.covidsafe.bluetooth.gatt
|
|
||||||
|
|
||||||
import android.bluetooth.BluetoothGattCharacteristic
|
|
||||||
import android.bluetooth.BluetoothGattService
|
|
||||||
import android.content.Context
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.properties.Delegates
|
|
||||||
|
|
||||||
class GattService constructor(val context: Context, serviceUUIDString: String) {
|
|
||||||
|
|
||||||
private var serviceUUID = UUID.fromString(serviceUUIDString)
|
|
||||||
|
|
||||||
var gattService: BluetoothGattService by Delegates.notNull()
|
|
||||||
|
|
||||||
private var devicePropertyCharacteristic: BluetoothGattCharacteristic by Delegates.notNull()
|
|
||||||
|
|
||||||
init {
|
|
||||||
gattService = BluetoothGattService(serviceUUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
|
|
||||||
devicePropertyCharacteristic = BluetoothGattCharacteristic(
|
|
||||||
serviceUUID,
|
|
||||||
BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_WRITE,
|
|
||||||
BluetoothGattCharacteristic.PERMISSION_READ or BluetoothGattCharacteristic.PERMISSION_WRITE
|
|
||||||
)
|
|
||||||
gattService.addCharacteristic(devicePropertyCharacteristic)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setValue(value: String) {
|
|
||||||
setValue(value.toByteArray(Charsets.UTF_8))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setValue(value: ByteArray) {
|
|
||||||
devicePropertyCharacteristic.value = value
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,67 @@
|
||||||
|
// 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",
|
||||||
|
"^10....1C"
|
||||||
|
,"^07","^05","^09"
|
||||||
|
,"^00","^05","^09"
|
||||||
|
//Apple TV
|
||||||
|
,"^0101000000000000000000000000000000"
|
||||||
|
//no service
|
||||||
|
,"^0100000000000000000000000000000000"
|
||||||
|
,"^1002","^06","^07","^08","^03","^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,931 @@
|
||||||
|
// 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.minutes(5).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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
val data = SignalCharacteristicData.encodeWritePayload(transmitter.payloadData())
|
||||||
|
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 getWritePayloadForCentral(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()))
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
return writedata
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeSignalCharacteristic(gatt: BluetoothGatt, task: NextTask, data: ByteArray?) {
|
||||||
|
val device = database.device(gatt.device)
|
||||||
|
val signalCharacteristic = device.signalCharacteristic()
|
||||||
|
if (signalCharacteristic == null) {
|
||||||
|
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,558 @@
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
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()
|
||||||
|
?: //logger.fault("Bluetooth adapter unavailable")
|
||||||
|
return null
|
||||||
|
return if (!bluetoothAdapter.isMultipleAdvertisementSupported) {
|
||||||
|
//logger.fault("Bluetooth advertisement unsupported")
|
||||||
|
null
|
||||||
|
} else bluetoothAdapter.bluetoothLeAdvertiser
|
||||||
|
?: //logger.fault("Bluetooth advertisement unavailable")
|
||||||
|
return 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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) {
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
if (characteristic.uuid !== BLESensorConfiguration.androidSignalCharacteristicUUID) {
|
||||||
|
if (responseNeeded) {
|
||||||
|
server.get()?.sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, offset, value)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val data = Data(onCharacteristicWriteSignalData(device, value))
|
||||||
|
if (characteristic.uuid == BLESensorConfiguration.legacyCovidsafePayloadCharacteristicUUID) {
|
||||||
|
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_SUCCESS, 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()
|
||||||
|
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)
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// Battery log CSV is for debug purposes. This will be removed in the production build.
|
||||||
|
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,149 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
/// Concrete Sensor log is for debug purposes. This will be removed in the production build.
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// Contact log CSV is for debug purposes. This will be removed in the production build.
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// Detection log CSV is for debug purposes. This will be removed in the production build.
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// Statistics log CSV is for debug purposes. This will be removed in the production build.
|
||||||
|
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,57 @@
|
||||||
|
// Copyright 2020 VMware, Inc.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
|
||||||
|
package au.gov.health.covidsafe.sensor.datatype;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
public PseudoDeviceAddress() {
|
||||||
|
// Bluetooth device address is 48-bit (6 bytes), using
|
||||||
|
// the same length to offer the same collision avoidance
|
||||||
|
address = Math.round(Math.random() * Math.pow(2, 48));
|
||||||
|
final ByteBuffer byteBuffer = ByteBuffer.allocate(8);
|
||||||
|
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
byteBuffer.putLong(0, address);
|
||||||
|
// Only taking last 6 bytes as that is the maximum value
|
||||||
|
data = new byte[6];
|
||||||
|
System.arraycopy(byteBuffer.array(), 2, data, 0, data.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PseudoDeviceAddress(final byte[] data) {
|
||||||
|
this.data = data;
|
||||||
|
final ByteBuffer byteBuffer = ByteBuffer.allocate(8);
|
||||||
|
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
byteBuffer.position(2);
|
||||||
|
byteBuffer.put(data);
|
||||||
|
this.address = 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() {
|
||||||
|
@ -239,18 +206,15 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
|
||||||
|
|
||||||
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 +233,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,17 +252,47 @@ 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() {
|
||||||
Utils.scheduleBMUpdateCheck(this.applicationContext, bmCheckInterval)
|
Utils.scheduleBMUpdateCheck(this.applicationContext, bmCheckInterval)
|
||||||
|
|
||||||
|
@ -330,102 +313,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 +330,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 +339,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 +401,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 +429,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 +477,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 +492,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 +632,23 @@ 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,20 +149,26 @@ 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
|
||||||
// readCharacteristic and writeCharacteristic. IBluetoothGattInvocationHandler will catch
|
// twice on the same BluetoothGatt instance.
|
||||||
// calls to these functions and rewrite their authReq field to ensure no pairing attempts
|
if (Proxy.isProxyClass(mService.javaClass)) {
|
||||||
// occur
|
CentralLog.i(TAG,
|
||||||
val mServiceProxy = Proxy.newProxyInstance(gatt.javaClass.classLoader,
|
"Not proxying this mService as it is already proxied!")
|
||||||
Array(1) { iBluetoothGattClass!! },
|
} else {
|
||||||
IBluetoothGattInvocationHandler(mService))
|
// Wrap the IBluetoothGatt instance in a Proxy object in order to intercept calls to
|
||||||
|
// readCharacteristic and writeCharacteristic. IBluetoothGattInvocationHandler will catch
|
||||||
// Write the proxy back to BluetoothGatt.mService
|
// calls to these functions and rewrite their authReq field to ensure no pairing attempts
|
||||||
mServiceField!!.set(gatt, mServiceProxy)
|
// occur
|
||||||
|
val mServiceProxy = Proxy.newProxyInstance(gatt.javaClass.classLoader,
|
||||||
|
Array(1) { iBluetoothGattClass!! },
|
||||||
|
IBluetoothGattInvocationHandler(mService))
|
||||||
|
|
||||||
|
// Write the proxy back to BluetoothGatt.mService
|
||||||
|
mServiceField!!.set(gatt, mServiceProxy)
|
||||||
|
}
|
||||||
// Reset accessibility
|
// Reset accessibility
|
||||||
mServiceField!!.isAccessible = mServiceAccessible
|
mServiceField!!.isAccessible = mServiceAccessible
|
||||||
}
|
}
|
||||||
catch (e: IllegalAccessException) {
|
catch (e: IllegalAccessException) {
|
||||||
// Field was inaccessible when written
|
// Field was inaccessible when written
|
||||||
CentralLog.i(TAG,
|
CentralLog.i(TAG,
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -310,7 +310,7 @@ class StreetPassWorker(val context: Context) {
|
||||||
it.checklist.started.status = true
|
it.checklist.started.status = true
|
||||||
it.checklist.started.timePerformed = System.currentTimeMillis()
|
it.checklist.started.timePerformed = System.currentTimeMillis()
|
||||||
|
|
||||||
it.startWork(context, gattCallback)
|
it.startWork(context, gattCallback)
|
||||||
|
|
||||||
var connecting = it.gatt?.connect() ?: false
|
var connecting = it.gatt?.connect() ?: false
|
||||||
|
|
||||||
|
@ -646,7 +646,7 @@ class StreetPassWorker(val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class EncryptedWriteRequestPayload(val timestamp: Long, val modelC: String, val rssi: Int, val txPower: Int?, val msg: String)
|
inner class EncryptedWriteRequestPayload(val timestamp: Long, val modelC: String, val rssi: Int, val txPower: Int?, val msg: String?)
|
||||||
|
|
||||||
override fun onCharacteristicWrite(
|
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 = 89
|
||||||
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>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue