Herald debug code for tech community

This commit is contained in:
covidsafe-support 2020-11-29 13:01:13 -08:00
parent 6bf46ded07
commit 2ef00979e5
105 changed files with 6184 additions and 975 deletions

View file

@ -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 VMwares 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.

View file

@ -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

View file

@ -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')"
]
}
}

View file

@ -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')"
]
}
}

View file

@ -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
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}
}

View file

@ -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

View file

@ -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) {
}
} }

View file

@ -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")
} }
} }
} }

View file

@ -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}")
}
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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

View file

@ -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) {
}
}

View 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();
}

View file

@ -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();
}
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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();
}
}

View file

@ -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
}

View file

@ -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);
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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<>();
}

View file

@ -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 {
}

View file

@ -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"
};
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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);
}
}
});
}
}

View file

@ -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
}

View file

@ -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);
}
}
}

View file

@ -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())
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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() +
'}';
}
}

View file

@ -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;
}
}

View file

@ -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() +
'}';
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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()));
}
}

View file

@ -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();
}
}
}
}

View file

@ -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);
}

View file

@ -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
}

View file

@ -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());
}
}
}

View file

@ -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;
}
}
}

View file

@ -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();
}
}

View file

@ -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
}

View file

@ -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);
}

View file

@ -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();
}
}

View file

@ -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 + "]";
}
}

View file

@ -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();
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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());
}
}

View file

@ -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 + ")";
}
}

View file

@ -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();
}
}

View file

@ -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
}

View file

@ -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);
}
}

View file

@ -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 +
'}';
}
}

View file

@ -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() + "]";
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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;
}
}
}

View file

@ -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
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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 +
'}';
}
}

View file

@ -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 + ")";
}
}

View file

@ -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());
}
}

View file

@ -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 + ")";
}
}

View file

@ -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 + ")";
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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 ""
}
} }
} }

View file

@ -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)

View file

@ -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,

View file

@ -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()
}
}

View file

@ -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,

View file

@ -25,5 +25,4 @@ class StreetPassRecordStorage(val context: Context) {
fun getAllRecords(): List<StreetPassRecord> { fun getAllRecords(): List<StreetPassRecord> {
return recordDao.getCurrentRecords() return recordDao.getCurrentRecords()
} }
} }

View file

@ -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() {

View file

@ -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)

View file

@ -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()) {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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