mobile-ios/CovidSafe/BluetraceManager.swift
2020-12-19 16:13:44 +11:00

225 lines
8.9 KiB
Swift

import UIKit
import CoreData
import CoreBluetooth
class BluetraceManager: SensorDelegate {
private let logger = ConcreteSensorLogger(subsystem: "Herald", category: "BluetraceManager")
var sensorDidUpdateStateCallback: ((SensorState, SensorType?) -> Void)?
// Payload data supplier, sensor and contact log
var payloadDataSupplier: PayloadDataSupplier?
var sensor: Sensor?
static let shared = BluetraceManager()
var bleSensorState: SensorState = .unavailable
var awakeSensorState: SensorState = .unavailable
private var didRecordPayloadData: [String:Date] = [:]
private var didWriteLegacyPayloadData: [String:Date] = [:]
func turnOnBLE() {
payloadDataSupplier = EncounterMessageManager.shared
if sensor == nil {
sensor = SensorArray(payloadDataSupplier!)
sensor?.add(delegate: self)
}
sensor?.start()
}
func turnOnAllSensors() {
payloadDataSupplier = EncounterMessageManager.shared
if sensor == nil {
sensor = SensorArray(payloadDataSupplier!)
sensor?.add(delegate: self)
// we are going to turn on one at a time
let previousSensorDidUpdateStateCallback = sensorDidUpdateStateCallback
sensorDidUpdateStateCallback = { state, type in
if (state == .on ) {
self.sensorDidUpdateStateCallback = previousSensorDidUpdateStateCallback
self.turnOnLocationSensor()
}
}
}
sensor?.start()
}
func turnOnLocationSensor() {
guard let sensorArray = sensor as? SensorArray else {
return
}
DispatchQueue.main.async {
sensorArray.startAwakeSensor()
}
}
func isBluetoothAuthorized() -> Bool {
if #available(iOS 13.1, *) {
return CBManager.authorization == .allowedAlways
} else {
return CBPeripheralManager.authorizationStatus() == .authorized
}
}
func isBluetoothOn() -> Bool {
return bleSensorState == .on
}
func isLocationOnAuthorized() -> Bool {
return awakeSensorState == .on
}
func centralDidUpdateStateCallback(_ state: CBManagerState) {
if state == .poweredOn {
sensorDidUpdateStateCallback?(.on, .BLE)
} else {
sensorDidUpdateStateCallback?(.off, .BLE)
}
}
func toggleAdvertisement(_ state: Bool) {
if state {
sensor?.start()
} else {
sensor?.stop()
}
}
func toggleScanning(_ state: Bool) {
if state {
sensor?.start()
} else {
sensor?.stop()
}
}
func cleanRecordsData( records: inout [String:Date], expiryInterval: TimeInterval) {
let removeDataBefore = Date() - expiryInterval
let recentDidWritePayloadData = records.filter({ $0.value >= removeDataBefore })
records = recentDidWritePayloadData
}
func saveEncounter(payload: PayloadData, proximity: Proximity, txPower: Int? ) throws {
let peripheralCharData = try JSONDecoder().decode(PeripheralCharacteristicsData.self, from: payload)
var encounterStruct = EncounterRecord(rssi: proximity.value, txPower: txPower == nil ? nil : Double(txPower!))
encounterStruct.msg = peripheralCharData.msg
encounterStruct.update(modelP: peripheralCharData.modelP)
encounterStruct.org = peripheralCharData.org
encounterStruct.v = peripheralCharData.v
encounterStruct.timestamp = Date()
// here the remote blob will be msg and modelp if v1, msg if v2
// local blob will be rssi, txpower, modelc
try encounterStruct.saveRemotePeripheralToCoreData()
}
func getIdentifier(forDevice: BLEDevice) -> String {
return forDevice.pseudoDeviceAddress != nil ? "\(forDevice.pseudoDeviceAddress!.address)" : forDevice.identifier
}
func shouldSaveEncounter(forDeviceIdentifier: String) -> Bool {
guard didRecordPayloadData[forDeviceIdentifier] == nil || abs(didRecordPayloadData[forDeviceIdentifier]!.timeIntervalSinceNow) > BluetraceConfig.PeripheralPayloadSaveInterval else {
return false
}
return true
}
// MARK:- SensorDelegate
func sensor(_ sensor: SensorType, didDetect: TargetIdentifier) {
logger.info(sensor.rawValue + ",didDetect=" + didDetect.description)
}
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier) {
logger.info(sensor.rawValue + ",didMeasure=" + didMeasure.description + ",fromTarget=" + fromTarget.description)
// Make a call to the messages API if needed.
// Dispatch in background queue
DispatchQueue.global(qos: .background).async {
MessageAPI.getMessagesIfNeeded() { (messageResponse, error) in
if let error = error {
DLog("Get messages error: \(error.localizedDescription)")
}
// We currently dont do anything with the response. Messages are delivered via APN
}
}
}
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier, withPayload: PayloadData, forDevice: BLEDevice) {
logger.info(sensor.rawValue + ",didMeasure=" + didMeasure.description + ",fromTarget=" + fromTarget.description + ",withPayload=" + withPayload.shortName)
let deviceIdentifier = getIdentifier(forDevice: forDevice)
guard shouldSaveEncounter(forDeviceIdentifier: deviceIdentifier) else {
logger.info("didMeasure, skipping save as time since last saved too recet fromTarget=" + fromTarget.description)
return
}
didRecordPayloadData[deviceIdentifier] = Date()
cleanRecordsData(records: &didRecordPayloadData, expiryInterval: BluetraceConfig.PeripheralPayloadExpiry)
do {
logger.debug("Saving on didMeasure fromTarget=" + fromTarget.description)
try saveEncounter(payload: withPayload, proximity: didMeasure, txPower: nil)
} catch {
logger.fault("ERROR "+sensor.rawValue + ",didMeasure=" + didMeasure.description + ",fromTarget=" + fromTarget.description + ",withPayload=" + withPayload.shortName )
}
}
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier) {
do {
let dataFromCentral = try JSONDecoder().decode(CentralWriteData.self, from: didRead)
logger.info(sensor.rawValue + ",didRead=" + dataFromCentral.msg + ",fromTarget=" + fromTarget.description)
} catch {
logger.fault(sensor.rawValue + ",didRead=" + didRead.shortName + ",fromTarget=" + fromTarget.description)
}
}
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier, atProximity: Proximity, withTxPower: Int?) {
logger.info(sensor.rawValue + ",didRead=\(didRead.shortName))" + ",fromTarget=" + fromTarget.description + ",atProximity=" + atProximity.description + ",withTxPower=\(String(describing: withTxPower))")
}
func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier, atProximity: Proximity) {
let payloads = didShare.map { $0.shortName }
logger.info(sensor.rawValue + ",didShare=" + payloads.description + ",fromTarget=" + fromTarget.description)
}
func sensor(_ sensor: SensorType, didUpdateState: SensorState) {
logger.info(sensor.rawValue + ",didUpdateState=" + didUpdateState.rawValue)
if sensor == .BLE {
bleSensorState = didUpdateState
sensorDidUpdateStateCallback?(didUpdateState, sensor)
}
if sensor == .AWAKE {
awakeSensorState = didUpdateState
sensorDidUpdateStateCallback?(didUpdateState, sensor)
}
}
func shouldWriteToLegacyDevice(_ device: BLEDevice) -> Bool {
guard device.legacyPayloadCharacteristic != nil &&
device.payloadCharacteristic == nil else {
return false
}
let cleanInterval = BluetraceConfig.PeripheralPayloadExpiry
let writeInterval = BluetraceConfig.PeripheralPayloadSaveInterval
let deviceIdentifier = getIdentifier(forDevice: device)
cleanRecordsData(records: &didWriteLegacyPayloadData, expiryInterval: cleanInterval)
guard didWriteLegacyPayloadData[deviceIdentifier] == nil || abs(didWriteLegacyPayloadData[deviceIdentifier]!.timeIntervalSinceNow) > writeInterval else {
return false
}
return true
}
func didWriteToLegacyDevice(_ device: BLEDevice) {
let deviceIdentifier = getIdentifier(forDevice: device)
didWriteLegacyPayloadData[deviceIdentifier] = Date()
}
}