Skip to content

Commit

Permalink
Merge pull request #194 from theheraldproject/develop
Browse files Browse the repository at this point in the history
PR from develop to main for v2.1.0
  • Loading branch information
adamfowleruk authored Jan 14, 2023
2 parents f97d05b + 3fca6a0 commit 9afaff4
Show file tree
Hide file tree
Showing 56 changed files with 2,984 additions and 3,747 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ on:
pull_request:
branches:
- develop # PRs from external developers or project team
- master # PRs for triggering a release. Both old (master) and new (main) branch names
- main
- main # PRs for triggering a release

jobs:
unit_tests:
Expand All @@ -24,7 +23,8 @@ jobs:
destination: [
#'platform=iOS Simulator,OS=12.2,name=iPhone X'
#,
'platform=iOS Simulator,OS=14.4,name=iPhone 11 Pro'
# 'platform=iOS Simulator,OS=15.2,name=iPhone 11 Pro'
'platform=iOS Simulator,OS=16.0,name=iPhone 11 Pro'
#,
#'platform=iOS Simulator,OS=9.3,name=iPhone 5s'
]
Expand Down
8 changes: 6 additions & 2 deletions Herald-for-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
6FBC6F2625868ED8001A86CE /* ModeSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBC6F2525868ED8001A86CE /* ModeSelectionViewController.swift */; };
6FBC6F2825868FAF001A86CE /* VenueModeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBC6F2725868FAF001A86CE /* VenueModeViewController.swift */; };
6FBC6F2C2586B8D8001A86CE /* TargetDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBC6F2B2586B8D8001A86CE /* TargetDetailsViewController.swift */; };
B62983CB267F575800C88B9D /* AutomatedTestClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62983CA267F575800C88B9D /* AutomatedTestClient.swift */; };
B68215BD251E789C0091DE22 /* Herald.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B68215BB251E78840091DE22 /* Herald.framework */; };
B68215BE251E789C0091DE22 /* Herald.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B68215BB251E78840091DE22 /* Herald.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
B692CA9424CADA7500F45AEE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B692CA9324CADA7500F45AEE /* AppDelegate.swift */; };
Expand Down Expand Up @@ -59,6 +60,7 @@
6FBC6F2525868ED8001A86CE /* ModeSelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModeSelectionViewController.swift; sourceTree = "<group>"; };
6FBC6F2725868FAF001A86CE /* VenueModeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VenueModeViewController.swift; sourceTree = "<group>"; };
6FBC6F2B2586B8D8001A86CE /* TargetDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetDetailsViewController.swift; sourceTree = "<group>"; };
B62983CA267F575800C88B9D /* AutomatedTestClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomatedTestClient.swift; sourceTree = "<group>"; };
B68215B6251E78840091DE22 /* Herald.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Herald.xcodeproj; path = herald/Herald.xcodeproj; sourceTree = "<group>"; };
B6894BC3251B33E500BDFB1E /* Herald.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Herald.framework; sourceTree = BUILT_PRODUCTS_DIR; };
B6894BD0251B356000BDFB1E /* Herald.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Herald.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -135,6 +137,7 @@
6FBC6F2B2586B8D8001A86CE /* TargetDetailsViewController.swift */,
6F47E13D25871B29003D3042 /* VenueDiaryViewController.swift */,
6F47E14125871EE4003D3042 /* VenueDiaryEventCell.swift */,
B62983CA267F575800C88B9D /* AutomatedTestClient.swift */,
);
path = "Herald-for-iOS";
sourceTree = "<group>";
Expand Down Expand Up @@ -239,6 +242,7 @@
B692CA9824CADA7500F45AEE /* PhoneModeViewController.swift in Sources */,
6F47E14225871EE4003D3042 /* VenueDiaryEventCell.swift in Sources */,
B692CA9424CADA7500F45AEE /* AppDelegate.swift in Sources */,
B62983CB267F575800C88B9D /* AutomatedTestClient.swift in Sources */,
6F47E13E25871B29003D3042 /* VenueDiaryViewController.swift in Sources */,
6FBC6F2825868FAF001A86CE /* VenueModeViewController.swift in Sources */,
6FBC6F2625868ED8001A86CE /* ModeSelectionViewController.swift in Sources */,
Expand Down Expand Up @@ -395,7 +399,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.0;
MARKETING_VERSION = 2.1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.heraldprox.herald.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
Expand All @@ -416,7 +420,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.0;
MARKETING_VERSION = 2.1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.heraldprox.herald.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
Expand Down
34 changes: 27 additions & 7 deletions Herald-for-iOS/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, SensorDelegate {
// Payload data supplier, sensor and contact log
var payloadDataSupplier: PayloadDataSupplier?
var sensor: SensorArray?

var phoneMode = true

// Test automation, set to null to disable automation.
// Set to "http://serverAddress:port" to enable automation.
let automatedTestServer: String? = nil
var automatedTestClient: AutomatedTestClient? = nil

/// Generate unique and consistent device identifier for testing detection and tracking
private func identifier() -> Int32 {
Expand All @@ -43,9 +47,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate, SensorDelegate {
payloadDataSupplier = ConcreteTestPayloadDataSupplier(identifier: identifier())
sensor = SensorArray(payloadDataSupplier!)
sensor?.add(delegate: self)
// Sensor will start and stop with UI switch (default ON) and bluetooth state
// Or remotely controlled by test server.
if let automatedTestServer = automatedTestServer, let sensorArray = sensor {
automatedTestClient = AutomatedTestClient(serverAddress: automatedTestServer, sensorArray: sensorArray, heartbeatInterval: TimeInterval(10))
if let automatedTestClient = automatedTestClient {
sensor?.add(delegate: automatedTestClient)
}
} else {
sensor?.start()
}
addEfficacyLogging()
sensor?.start()


// EXAMPLE immediate data send function (note: NOT wrapped with Herald header)
//let targetIdentifier: TargetIdentifier? // ... set its value
//let success: Bool = sensor!.immediateSend(data: Data(), targetIdentifier!)
Expand Down Expand Up @@ -84,13 +98,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate, SensorDelegate {
if let payloadData = sensor?.payloadData {
// Loggers
#if DEBUG
sensor?.add(delegate: ContactLog(filename: "contacts.csv"))
sensor?.add(delegate: StatisticsLog(filename: "statistics.csv", payloadData: payloadData))
sensor?.add(delegate: DetectionLog(filename: "detection.csv", payloadData: payloadData))
_ = BatteryLog(filename: "battery.csv")
var sensorDelegateLoggers: [SensorDelegateLogger] = []
sensorDelegateLoggers.append(ContactLog(filename: "contacts.csv"))
// Removed to align with removal in Android for bug https://github.com/theheraldproject/herald-for-android/issues/239
// sensorDelegateLoggers.append(StatisticsLog(filename: "statistics.csv", payloadData: payloadData))
sensorDelegateLoggers.append(DetectionLog(filename: "detection.csv", payloadData: payloadData))
sensorDelegateLoggers.append(BatteryLog(filename: "battery.csv"))
if (BLESensorConfiguration.payloadDataUpdateTimeInterval != .never ||
(BLESensorConfiguration.interopOpenTraceEnabled && BLESensorConfiguration.interopOpenTracePayloadDataUpdateTimeInterval != .never)) {
sensor?.add(delegate: EventTimeIntervalLog(filename: "statistics_didRead.csv", payloadData: payloadData, eventType: .read))
sensorDelegateLoggers.append(EventTimeIntervalLog(filename: "statistics_didRead.csv", payloadData: payloadData, eventType: .read))
}
for sensorDelegateLogger in sensorDelegateLoggers {
sensor?.add(delegate: sensorDelegateLogger)
automatedTestClient?.add(sensorDelegateLogger)
}
#endif
}
Expand Down
205 changes: 205 additions & 0 deletions Herald-for-iOS/AutomatedTestClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//
// AutomatedTestClient.swift
//
// Copyright 2021 Herald Project Contributors
// SPDX-License-Identifier: Apache-2.0
//

import Foundation
import UIKit
import Herald

class AutomatedTestClient: SensorDelegate {
private let logger = Log(subsystem: "Herald", category: "AutomatedTestClient")
private let serverAddress: String
private let sensorArray: SensorArray
private let heartbeatInterval: TimeInterval
private var timerThread: Timer?
private var resettables: [Resettable] = []
private var commandQueue: [String] = []
private let executorService = DispatchQueue(label: "App.AutomatedTestClient")
private var sensorArrayState: Bool = false
private var lastTimerCallback: Date = Date.distantPast
private var lastActionHeartbeat: Date = Date.distantPast
private var processingQueue: Bool = false


init(serverAddress: String, sensorArray: SensorArray, heartbeatInterval: TimeInterval) {
self.serverAddress = (serverAddress.hasSuffix("/") ? serverAddress : "\(serverAddress)/")
self.sensorArray = sensorArray
self.heartbeatInterval = heartbeatInterval
self.timerThread = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.timerCallback), userInfo: nil, repeats: true)
// Add resettable
add(ConcreteSensorLogger(subsystem: "App", category: "AutomatedTestClient"))
}

/// Timer callback to trigger process queue once per second
@objc func timerCallback() {
let now = Date()
let elapsed = now.timeIntervalSince(lastTimerCallback)
guard elapsed >= TimeInterval(1) else {
return
}
executorService.async {
guard now.timeIntervalSince(self.lastActionHeartbeat) > self.heartbeatInterval else {
return
}
self.actionHeartbeat()
self.lastActionHeartbeat = now
}
lastTimerCallback = now
}

// MARK: - SensorDelegate

func sensor(_ sensor: SensorType, didUpdateState: SensorState) {
guard sensor == .ARRAY else {
return
}
sensorArrayState = (didUpdateState == .on)
logger.debug("sensor (didUpdateState=\(didUpdateState))")
actionHeartbeat()
}

/// Add resettable item for resetting on clear action.
func add(_ resettable: Resettable) {
resettables.append(resettable)
}

func processQueue() {
guard !processingQueue else {
return
}
while !commandQueue.isEmpty {
processingQueue = true
let command = commandQueue.removeFirst()
if "start" == command {
logger.debug("processQueue, processing (command=start,action=startSensorArray)")
sensorArray.start()
} else if "stop" == command {
logger.debug("processQueue, processing (command=stop,action=stopSensorArray)")
sensorArray.stop()
} else if command.starts(with: "upload") {
let filename = String(command.dropFirst("upload(".count).dropLast(1))
logger.debug("processQueue, processing (command=upload,action=uploadFile,filename=\(filename))")
actionUpload(filename)
} else if "clear" == command {
logger.debug("processQueue, processing (command=clear,action=clear)")
actionClear()
} else {
logger.fault("processQueue, ignoring unknown command (command=\(command))")
}
}
processingQueue = false
}

// MARK: - Actions

func actionHeartbeat() {
guard let payloadData = sensorArray.payloadData else {
return
}
serverHeartbeat(
model: UIDevice.current.name,
os: "iOS",
version: UIDevice.current.systemVersion,
payload: ConcretePayloadDataFormatter().shortFormat(payloadData),
status: (sensorArrayState ? "on" : "off"),
postProcess: processQueue)
}

func actionUpload(_ filename: String) {
guard let payloadData = sensorArray.payloadData else {
return
}
serverUpload(
model: UIDevice.current.name,
os: "iOS",
version: UIDevice.current.systemVersion,
payload: ConcretePayloadDataFormatter().shortFormat(payloadData),
status: (sensorArrayState ? "on" : "off"),
filename: filename)
}

func actionClear() {
resettables.forEach({ $0.reset() })
}

// MARK: - Server API

private func serverHeartbeat(model: String, os: String, version: String, payload: String, status: String, postProcess: @escaping () -> Void) {
let urlString = "\(serverAddress)heartbeat?model=\(model.addingPercentEncoding(withAllowedCharacters: .rfc3986Unreserved)!)&os=\(os.addingPercentEncoding(withAllowedCharacters: .rfc3986Unreserved)!)&version=\(version.addingPercentEncoding(withAllowedCharacters: .rfc3986Unreserved)!)&payload=\(payload.addingPercentEncoding(withAllowedCharacters: .rfc3986Unreserved)!)&status=\(status.addingPercentEncoding(withAllowedCharacters: .rfc3986Unreserved)!)"
guard let url = URL(string: urlString) else {
logger.fault("serverHeartbeat, invalid URL (url=\(urlString))")
return
}
let getUrlTask = URLSession.shared.dataTask(with: url) { data, responseStatusCode, error in
self.lastActionHeartbeat = Date()
guard let data = data, error == nil else {
self.logger.fault("serverHeartbeat, failed to get URL (url=\(url))")
return
}
guard let response = String(data: data, encoding: .utf8) else {
self.logger.fault("serverHeartbeat, failed to decode response data")
return
}
guard !response.isEmpty else {
self.logger.fault("serverHeartbeat, no response from server")
return
}
guard response.starts(with: "ok") else {
self.logger.fault("serverHeartbeat, server responded with error (response=\(response))")
return
}
let commands: [String] = response.split(separator: ",").map({ String($0) })
if commands.count > 1 {
for i in 1...commands.count-1 {
self.commandQueue.append(commands[i])
}
}
self.logger.debug("serverHeartbeat, complete (commandQueue=\(self.commandQueue))")
postProcess()
self.logger.debug("serverHeartbeat, post process complete")
}
getUrlTask.resume()
}

private func serverUpload(model: String, os: String, version: String, payload: String, status: String, filename: String) {
let urlString = "\(serverAddress)upload?model=\(model.addingPercentEncoding(withAllowedCharacters: .rfc3986Unreserved)!)&os=\(os.addingPercentEncoding(withAllowedCharacters: .rfc3986Unreserved)!)&version=\(version.addingPercentEncoding(withAllowedCharacters: .rfc3986Unreserved)!)&payload=\(payload.addingPercentEncoding(withAllowedCharacters: .rfc3986Unreserved)!)&status=\(status.addingPercentEncoding(withAllowedCharacters: .rfc3986Unreserved)!)&filename=\(filename.addingPercentEncoding(withAllowedCharacters: .rfc3986Unreserved)!)"
guard let url = URL(string: urlString) else {
logger.fault("serverUpload, invalid URL (url=\(urlString))")
return
}
guard let fileUrl = TextFile(filename: filename).url else {
logger.fault("serverUpload, invalid file (filename=\(filename))")
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
let postUrlTask = URLSession.shared.uploadTask(with: request, fromFile: fileUrl) { data, responseStatusCode, error in
self.lastActionHeartbeat = Date()
guard let data = data, error == nil else {
self.logger.fault("serverUpload, failed to get URL (url=\(url))")
return
}
guard let response = String(data: data, encoding: .utf8) else {
self.logger.fault("serverUpload, failed to decode response data")
return
}
guard !response.isEmpty else {
self.logger.fault("serverUpload, no response from server")
return
}
guard response.starts(with: "ok") else {
self.logger.fault("serverUpload, server responded with error (response=\(response))")
return
}
self.logger.debug("serverUpload, complete (response=\(response))")
}
postUrlTask.resume()
}
}

extension CharacterSet {
static let rfc3986Unreserved = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~")
}
3 changes: 2 additions & 1 deletion Herald-for-iOS/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@
<action selector="showVenueDiary:" destination="BYZ-38-t0r" eventType="touchUpInside" id="6Lm-aM-QBe"/>
</connections>
</button>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NDT-aw-Yt1" userLabel="sensorOnOffSwitch">
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="NDT-aw-Yt1" userLabel="sensorOnOffSwitch">
<rect key="frame" x="345" y="64" width="51" height="31"/>
<connections>
<action selector="sensorOnOffSwitchAction:" destination="BYZ-38-t0r" eventType="valueChanged" id="U0A-rs-B0f"/>
Expand Down Expand Up @@ -530,6 +530,7 @@
<outlet property="labelSocialMixingScore09" destination="3s5-0h-FGs" id="irB-i6-07a"/>
<outlet property="labelSocialMixingScore10" destination="Pbe-8M-Mb4" id="QvJ-KF-eqo"/>
<outlet property="labelSocialMixingScore11" destination="he4-5I-bHb" id="ldI-PR-HVX"/>
<outlet property="switchSensorOnOff" destination="NDT-aw-Yt1" id="9lA-3S-EpU"/>
<outlet property="tableViewTargets" destination="mT0-iS-Edp" id="Vdj-oY-qC3"/>
<outlet property="textMessageToSend" destination="sap-g3-GwH" id="CZo-kQ-6Uy"/>
</connections>
Expand Down
5 changes: 5 additions & 0 deletions Herald-for-iOS/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Detect close contacts with other users to trace direct disease transmissions</string>
<key>NSBluetoothPeripheralUsageDescription</key>
Expand Down
Loading

0 comments on commit 9afaff4

Please sign in to comment.