2020-05-08 17:49:14 +10:00
//
// C e n t r a l C o n t r o l l e r . s w i f t
// C o v i d S a f e
//
// C o p y r i g h t © 2 0 2 0 A u s t r a l i a n G o v e r n m e n t . A l l r i g h t s r e s e r v e d .
//
import Foundation
import CoreData
import CoreBluetooth
import UIKit
struct CentralWriteData : Codable {
var modelC : String // p h o n e m o d e l o f c e n t r a l
var rssi : Double
var txPower : Double ?
var msg : String // t e m p I D
var org : String
var v : Int
}
class CentralController : NSObject {
enum CentralError : Error {
case centralAlreadyOn
case centralAlreadyOff
}
var centralDidUpdateStateCallback : ( ( CBManagerState ) -> Void ) ?
var characteristicDidReadValue : ( ( EncounterRecord ) -> Void ) ?
private let restoreIdentifierKey = " com.joelkek.tracer.central "
private var central : CBCentralManager ?
private var recoveredPeripherals : [ CBPeripheral ] = [ ]
private var queue : DispatchQueue
// T h i s d i c t i s t o k e e p t r a c k o f d i s c o v e r e d a n d r o i d d e v i c e s , s o t h a t i d o n o t c o n n e c t t o t h e s a m e a n d r o i d d e v i c e m u l t i p l e t i m e s w i t h i n t h e s a m e B l u e t r a c e C o n f i g . C e n t r a l S c a n I n t e r v a l
private var discoveredAndroidPeriManufacturerToUUIDMap = [ Data : UUID ] ( )
// T h i s d i c t h a s 2 p u r p o s e
// 1 . T o s t o r e a l l t h e E n c o u n t e r R e c o r d , b e c a u s e t h e R S S I a n d T x P o w e r i s g o t t e n a t t h e d i d D i s c o v e r P e r i p h e r a l d e l e g a t e , b u t t h e c h a r a c t e r s t i c v a l u e i s g o t t e n a t d i d U p d a t e V a l u e F o r C h a r a c t e r i s t i c d e l e g a t e
// 2 . U s e t o c h e c k f o r d u p l i c a t e d i p h o n e s p e r i p h e r a l b e i n g d i s c o v e r e d , s o t h a t i d o n t c o n n e c t t o t h e s a m e i p h o n e a g a i n i n t h e s a m e s c a n w i n d o w
private var scannedPeripherals = [ UUID : ( peripheral : CBPeripheral , encounter : EncounterRecord ) ] ( ) // s t o r e s t h e p e r i p h e r a l s e n c o u n t e r e d w i t h i n o n e s c a n i n t e r v a l
var timerForScanning : Timer ?
public init ( queue : DispatchQueue ) {
self . queue = queue
super . init ( )
}
func turnOn ( ) {
DLog ( " CC requested to be turnOn " )
guard central = = nil else {
return
}
2020-05-15 00:47:40 -07:00
let options : [ String : Any ] = [ CBCentralManagerOptionRestoreIdentifierKey : restoreIdentifierKey ,
CBCentralManagerOptionShowPowerAlertKey : NSNumber ( true ) ]
central = CBCentralManager ( delegate : self , queue : self . queue , options : options )
2020-05-08 17:49:14 +10:00
}
func turnOff ( ) {
DLog ( " CC turnOff " )
guard central != nil else {
return
}
central ? . stopScan ( )
central = nil
}
2020-05-15 00:47:40 -07:00
func shouldRecordEncounter ( _ encounter : EncounterRecord ) -> Bool {
guard let scannedDate = encounter . timestamp else {
return true
}
if abs ( scannedDate . timeIntervalSinceNow ) > BluetraceConfig . CentralScanInterval {
return true
}
return false
2020-05-08 17:49:14 +10:00
}
2020-05-15 00:47:40 -07:00
func shouldReconnectToPeripheral ( peripheral : CBPeripheral ) -> Bool {
guard let encounteredPeripheral = scannedPeripherals [ peripheral . identifier ] else {
return true
2020-05-08 17:49:14 +10:00
}
2020-05-15 00:47:40 -07:00
guard let scannedDate = encounteredPeripheral . encounter . timestamp else {
return true
}
if abs ( scannedDate . timeIntervalSinceNow ) > BluetraceConfig . CentralScanInterval {
return true
2020-05-08 17:49:14 +10:00
}
2020-05-15 00:47:40 -07:00
return false
}
public func getState ( ) -> CBManagerState ? {
return central ? . state
2020-05-08 17:49:14 +10:00
}
}
extension CentralController : CBCentralManagerDelegate {
2020-05-15 00:47:40 -07:00
func centralManager ( _ central : CBCentralManager , willRestoreState dict : [ String : Any ] ) {
DLog ( " CC willRestoreState. Central state: \( BluetraceUtils . centralStateToString ( central . state ) ) " )
if let peripheralsObject = dict [ CBCentralManagerRestoredStatePeripheralsKey ] {
let peripherals = peripheralsObject as ! Array < CBPeripheral >
DLog ( " CC restoring \( peripherals . count ) peripherals from system. " )
for peripheral in peripherals {
recoveredPeripherals . append ( peripheral )
peripheral . delegate = self
}
}
}
2020-05-08 17:49:14 +10:00
func centralManagerDidUpdateState ( _ central : CBCentralManager ) {
centralDidUpdateStateCallback ? ( central . state )
switch central . state {
case . poweredOn :
2020-05-15 00:47:40 -07:00
DLog ( " CC Starting a scan " )
Encounter . timestamp ( for : . scanningStarted )
// f o r a l l p e r i p h e r a l s t h a t a r e n o t d i s c o n n e c t e d , d i s c o n n e c t t h e m
self . scannedPeripherals . forEach { ( scannedPeri ) in
central . cancelPeripheralConnection ( scannedPeri . value . peripheral )
2020-05-08 17:49:14 +10:00
}
2020-05-15 00:47:40 -07:00
// c l e a r a l l p e r i p h e r a l s , s u c h t h a t a n e w s c a n w i n d o w c a n t a k e p l a c e
self . scannedPeripherals = [ UUID : ( CBPeripheral , EncounterRecord ) ] ( )
self . discoveredAndroidPeriManufacturerToUUIDMap = [ Data : UUID ] ( )
// h a n d l e a s t a t e r e s t o r a t i o n s c e n a r i o
for recoveredPeripheral in recoveredPeripherals {
var restoredEncounter = EncounterRecord ( rssi : 0 , txPower : nil )
restoredEncounter . timestamp = nil
scannedPeripherals . updateValue ( ( recoveredPeripheral , restoredEncounter ) ,
forKey : recoveredPeripheral . identifier )
central . connect ( recoveredPeripheral )
}
central . scanForPeripherals ( withServices : [ BluetraceConfig . BluetoothServiceID ] , options : [ CBCentralManagerScanOptionAllowDuplicatesKey : NSNumber ( true ) ] )
2020-05-08 17:49:14 +10:00
default :
2020-05-15 00:47:40 -07:00
DLog ( " State chnged to \( central . state ) " )
2020-05-08 17:49:14 +10:00
}
}
func handlePeripheralOfUncertainStatus ( _ peripheral : CBPeripheral ) {
// I f n o t c o n n e c t e d t o P e r i p h e r a l , a t t e m p t c o n n e c t i o n a n d e x i t
if peripheral . state != . connected {
DLog ( " CC handlePeripheralOfUncertainStatus not connected " )
central ? . connect ( peripheral )
return
}
// I f d o n ' t k n o w a b o u t P e r i p h e r a l ' s s e r v i c e s , d i s c o v e r s e r v i c e s a n d e x i t
if peripheral . services = = nil {
DLog ( " CC handlePeripheralOfUncertainStatus unknown services " )
peripheral . discoverServices ( [ BluetraceConfig . BluetoothServiceID ] )
return
}
// I f P e r i p h e r a l ' s s e r v i c e s d o n ' t c o n t a i n t a r g e t I D , d i s c o n n e c t a n d r e m o v e , t h e n e x i t .
// I f i t d o e s c o n t a i n t a r g e t I D , d i s c o v e r c h a r a c t e r i s t i c s f o r s e r v i c e
guard let service = peripheral . services ? . first ( where : { $0 . uuid = = BluetraceConfig . BluetoothServiceID } ) else {
DLog ( " CC handlePeripheralOfUncertainStatus no matching Services " )
central ? . cancelPeripheralConnection ( peripheral )
return
}
DLog ( " CC handlePeripheralOfUncertainStatus discoverCharacteristics " )
peripheral . discoverCharacteristics ( [ BluetraceConfig . BluetoothServiceID ] , for : service )
// I f P e r i p h e r a l ' s s e r v i c e ' s c h a r a c t e r i s t i c s d o n ' t c o n t a i n t a r g e t I D , d i s c o n n e c t a n d r e m o v e , t h e n e x i t .
// I f i t d o e s c o n t a i n t a r g e t I D , r e a d v a l u e f o r c h a r a c t e r i s t i c
guard let characteristic = service . characteristics ? . first ( where : { $0 . uuid = = BluetraceConfig . BluetoothServiceID } ) else {
DLog ( " CC handlePeripheralOfUncertainStatus no matching Characteristics " )
central ? . cancelPeripheralConnection ( peripheral )
return
}
DLog ( " CC handlePeripheralOfUncertainStatus readValue " )
peripheral . readValue ( for : characteristic )
return
}
func centralManager ( _ central : CBCentralManager , didDiscover peripheral : CBPeripheral , advertisementData : [ String : Any ] , rssi RSSI : NSNumber ) {
let debugLogs = [ " CentralState " : BluetraceUtils . centralStateToString ( central . state ) ,
" peripheral " : peripheral ,
" advertisments " : advertisementData as AnyObject ] as AnyObject
DLog ( " \( debugLogs ) " )
2020-05-15 00:47:40 -07:00
var initialEncounter = EncounterRecord ( rssi : RSSI . doubleValue , txPower : advertisementData [ CBAdvertisementDataTxPowerLevelKey ] as ? Double )
initialEncounter . timestamp = nil
2020-05-08 17:49:14 +10:00
// i p h o n e s w i l l " m a s k " t h e p e r i p h e r a l ' s i d e n t i f i e r f o r a n d r o i d d e v i c e s , r e s u l t i n g i n t h e s a m e a n d r o i d d e v i c e b e i n g d i s c o v e r e d m u l t i p l e t i m e s w i t h d i f f e r e n t p e r i p h e r a l i d e n t i f i e r . H e n c e A n d r o i d i s u s i n g u s e C B A d v e r t i s e m e n t D a t a S e r v i c e D a t a K e y d a t a f o r i d e n t i f y i n g a n a n d r o i d p h e r i p h e r a l
2020-05-15 00:47:40 -07:00
// A l s o , c h e c k t h a t t h e l e n g t h i s g r e a t e r t h a n 2 t o p r e v e n t c r a s h . O t h e r w i s e i g n o r e .
if let manuData = advertisementData [ CBAdvertisementDataManufacturerDataKey ] as ? Data , manuData . count > 2 {
2020-05-08 17:49:14 +10:00
let androidIdentifierData = manuData . subdata ( in : 2. . < manuData . count )
if discoveredAndroidPeriManufacturerToUUIDMap . keys . contains ( androidIdentifierData ) {
DLog ( " Android Peripheral \( peripheral ) has been discovered already in this window, will not attempt to connect to it again " )
return
} else {
peripheral . delegate = self
discoveredAndroidPeriManufacturerToUUIDMap . updateValue ( peripheral . identifier , forKey : androidIdentifierData )
2020-05-15 00:47:40 -07:00
scannedPeripherals . updateValue ( ( peripheral , initialEncounter ) , forKey : peripheral . identifier )
2020-05-08 17:49:14 +10:00
central . connect ( peripheral )
}
} else {
// M e a n s n o t a n d r o i d d e v i c e , i w i l l c h e c k i f t h e p e r i p h e r a l . i d e n t i f i e r e x i s t i n t h e s c a n n e d P e r i p h e r a l s
DLog ( " CBAdvertisementDataManufacturerDataKey Data not found. Peripheral is likely not android " )
2020-05-15 00:47:40 -07:00
if let encounteredPeripheral = scannedPeripherals [ peripheral . identifier ] {
if shouldReconnectToPeripheral ( peripheral : encounteredPeripheral . peripheral ) {
peripheral . delegate = self
if peripheral . state != . connected {
central . connect ( peripheral )
DLog ( " found previous peripheral from more than 60 seconds ago " )
}
} else {
DLog ( " iOS Peripheral \( peripheral ) has been discovered already in this window, will not attempt to connect to it again " )
if let scannedDate = encounteredPeripheral . encounter . timestamp {
DLog ( " It was found \( scannedDate . timeIntervalSinceNow ) seconds ago " )
}
}
} else {
2020-05-08 17:49:14 +10:00
peripheral . delegate = self
2020-05-15 00:47:40 -07:00
scannedPeripherals . updateValue ( ( peripheral , initialEncounter ) , forKey : peripheral . identifier )
2020-05-08 17:49:14 +10:00
central . connect ( peripheral )
}
}
}
func centralManager ( _ central : CBCentralManager , didConnect peripheral : CBPeripheral ) {
let peripheralStateString = BluetraceUtils . peripheralStateToString ( peripheral . state )
DLog ( " CC didConnect peripheral peripheralCentral state: \( BluetraceUtils . centralStateToString ( central . state ) ) , Peripheral state: \( peripheralStateString ) " )
2020-05-15 00:47:40 -07:00
guard shouldReconnectToPeripheral ( peripheral : peripheral ) else {
central . cancelPeripheralConnection ( peripheral )
return
}
2020-05-08 17:49:14 +10:00
peripheral . delegate = self
2020-05-15 00:47:40 -07:00
peripheral . readRSSI ( )
2020-05-08 17:49:14 +10:00
peripheral . discoverServices ( [ BluetraceConfig . BluetoothServiceID ] )
}
func centralManager ( _ central : CBCentralManager , didDisconnectPeripheral peripheral : CBPeripheral , error : Error ? ) {
DLog ( " CC didDisconnectPeripheral \( peripheral ) , \( error != nil ? " error: \( error . debugDescription ) " : " " ) " )
2020-05-18 12:39:27 +10:00
if #available ( iOS 12 , * ) {
let options = [ CBConnectPeripheralOptionStartDelayKey : NSNumber ( 15 ) ]
central . connect ( peripheral , options : options )
}
2020-05-08 17:49:14 +10:00
}
func centralManager ( _ central : CBCentralManager , didFailToConnect peripheral : CBPeripheral , error : Error ? ) {
DLog ( " CC didFailToConnect peripheral \( error != nil ? " error: \( error . debugDescription ) " : " " ) " )
}
}
extension CentralController : CBPeripheralDelegate {
2020-05-15 00:47:40 -07:00
func peripheral ( _ peripheral : CBPeripheral , didReadRSSI RSSI : NSNumber , error : Error ? ) {
if let err = error {
DLog ( " error: \( err ) " )
}
if error = = nil {
if let existingPeripheral = scannedPeripherals [ peripheral . identifier ] {
var scannedEncounter = existingPeripheral . encounter
scannedEncounter . rssi = RSSI . doubleValue
scannedPeripherals . updateValue ( ( existingPeripheral . peripheral , scannedEncounter ) , forKey : peripheral . identifier )
}
}
}
func peripheral ( _ peripheral : CBPeripheral , didModifyServices invalidatedServices : [ CBService ] ) {
DLog ( " Peripheral: \( peripheral ) didModifyServices: \( invalidatedServices ) " )
}
2020-05-08 17:49:14 +10:00
func peripheral ( _ peripheral : CBPeripheral , didDiscoverServices error : Error ? ) {
if let err = error {
DLog ( " error: \( err ) " )
}
guard let service = peripheral . services ? . first ( where : { $0 . uuid = = BluetraceConfig . BluetoothServiceID } ) else { return }
peripheral . discoverCharacteristics ( [ BluetraceConfig . BluetoothServiceID ] , for : service )
}
func peripheral ( _ peripheral : CBPeripheral , didDiscoverCharacteristicsFor service : CBService , error : Error ? ) {
if let err = error {
DLog ( " error: \( err ) " )
}
guard let characteristic = service . characteristics ? . first ( where : { $0 . uuid = = BluetraceConfig . BluetoothServiceID } ) else { return }
peripheral . readValue ( for : characteristic )
// D o n o t n e e d t o w a i t f o r a s u c c e s s f u l r e a d b e f o r e w r i t i n g , b e c a u s e n o d a t a f r o m t h e r e a d i s n e e d e d i n t h e w r i t e
if let currEncounter = scannedPeripherals [ peripheral . identifier ] {
EncounterMessageManager . shared . getTempId { ( result ) in
guard let tempId = result else {
DLog ( " broadcast msg not present " )
return
}
guard let rssi = currEncounter . encounter . rssi else {
DLog ( " rssi should be present in \( currEncounter . encounter ) " )
return
}
let dataToWrite = CentralWriteData ( modelC : DeviceIdentifier . getModel ( ) ,
rssi : rssi ,
txPower : currEncounter . encounter . txPower ,
msg : tempId ,
org : BluetraceConfig . OrgID ,
v : BluetraceConfig . ProtocolVersion )
do {
let encodedData = try JSONEncoder ( ) . encode ( dataToWrite )
peripheral . writeValue ( encodedData , for : characteristic , type : . withResponse )
} catch {
DLog ( " Error: \( error ) " )
}
}
}
}
func peripheral ( _ peripheral : CBPeripheral , didUpdateValueFor characteristic : CBCharacteristic , error : Error ? ) {
let debugLogs = [ " characteristic " : characteristic as AnyObject ,
" encounter " : scannedPeripherals [ peripheral . identifier ] as AnyObject ] as AnyObject
DLog ( " \( debugLogs ) " )
2020-05-15 00:47:40 -07:00
guard error = = nil else {
DLog ( " Error: \( String ( describing : error ) ) " )
return
}
if let scannedPeri = scannedPeripherals [ peripheral . identifier ] ,
let characteristicValue = characteristic . value ,
shouldRecordEncounter ( scannedPeri . encounter )
{
do {
let peripheralCharData = try JSONDecoder ( ) . decode ( PeripheralCharacteristicsData . self , from : characteristicValue )
var encounterStruct = scannedPeri . encounter
encounterStruct . msg = peripheralCharData . msg
encounterStruct . update ( modelP : peripheralCharData . modelP )
encounterStruct . org = peripheralCharData . org
encounterStruct . v = peripheralCharData . v
encounterStruct . timestamp = Date ( )
scannedPeripherals . updateValue ( ( scannedPeri . peripheral , encounterStruct ) , forKey : peripheral . identifier )
encounterStruct . saveToCoreData ( )
DLog ( " Central recorded encounter with \( String ( describing : scannedPeri . peripheral . name ) ) " )
} catch {
DLog ( " Error: \( error ) . CharacteristicValue is \( characteristicValue ) " )
2020-05-08 17:49:14 +10:00
}
} else {
2020-05-15 00:47:40 -07:00
DLog ( " Error: scannedPeripherals[peripheral.identifier] is \( String ( describing : scannedPeripherals [ peripheral . identifier ] ) ) , characteristic.value is \( String ( describing : characteristic . value ) ) " )
2020-05-08 17:49:14 +10:00
}
}
func peripheral ( _ peripheral : CBPeripheral , didWriteValueFor characteristic : CBCharacteristic , error : Error ? ) {
DLog ( " didWriteValueFor to peripheral: \( peripheral ) , for characteristics: \( characteristic ) . \( error != nil ? " error: \( error . debugDescription ) " : " " ) " )
central ? . cancelPeripheralConnection ( peripheral )
}
}