Sesame-iOS/Sesame-Watch Watch App/ContentView.swift
2023-08-14 10:39:29 +02:00

230 lines
6.9 KiB
Swift

import SwiftUI
import SFSafeSymbols
import CryptoKit
struct ContentView: View {
@Binding
var didLaunchFromComplication: Bool
@AppStorage("connectionType")
var connectionType: ConnectionStrategy = .remoteFirst
@AppStorage("server")
var serverPath: String = "https://christophhagen.de/sesame/"
@AppStorage("localIP")
var localAddress: String = "192.168.178.104/"
@AppStorage("counter")
var nextMessageCounter: Int = 0
@AppStorage("compensate")
var isCompensatingDaylightTime: Bool = false
@AppStorage("deviceId")
private var deviceId: Int = 0
@EnvironmentObject
var keyManager: KeyManagement
@EnvironmentObject
var history: HistoryManager
@State
var state: ClientState = .noKeyAvailable
@State
var stateResetTimer: Timer?
let server = Client()
private var firstTryIsLocalConnection: Bool {
switch connectionType {
case .local, .localFirst:
return true
case .remote, .remoteFirst:
return false
}
}
private var hasSecondTry: Bool {
switch connectionType {
case .localFirst, .remoteFirst:
return true
default:
return false
}
}
private var secondTryIsLocalConnection: Bool {
switch connectionType {
case .local, .localFirst:
return false
case .remote, .remoteFirst:
return true
}
}
var buttonBackground: Color {
state.allowsAction ?
.white.opacity(0.2) :
.black.opacity(0.2)
}
var buttonColor: Color {
state.allowsAction ? .white : .gray
}
var body: some View {
HStack {
Spacer()
VStack(alignment: .center) {
Image(systemSymbol: .lock)
.resizable()
.aspectRatio(contentMode: .fit)
.fontWeight(.ultraLight)
.padding()
.onTapGesture(perform: mainButtonPressed)
.disabled(!state.allowsAction)
if state == .waitingForResponse {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.frame(width: 20, height: 20)
} else {
Text(state.actionText)
.font(.subheadline)
}
}
Spacer()
}
.background(state.color)
.animation(.easeInOut, value: state.color)
.onAppear {
if state == .noKeyAvailable, keyManager.hasAllKeys {
state = .ready
}
}
.onChange(of: didLaunchFromComplication) { launched in
guard launched else {
return
}
didLaunchFromComplication = false
mainButtonPressed()
}
}
func mainButtonPressed() {
guard let keys = keyManager.getAllKeys(),
let deviceId = UInt8(exactly: deviceId) else {
return
}
sendMessage(from: deviceId, using: keys, isFirstTry: true)
}
private func sendMessage(from deviceId: UInt8, using keys: KeySet, isFirstTry: Bool) {
preventStateReset()
state = .waitingForResponse
let localConnection = isFirstTry ? firstTryIsLocalConnection : secondTryIsLocalConnection
Task {
let response = await send(
count: UInt32(nextMessageCounter),
from: deviceId,
using: keys,
to: server,
over: localConnection,
while: isCompensatingDaylightTime,
localAddress: localAddress,
remoteAddress: serverPath)
DispatchQueue.main.async {
state = response.response
scheduleStateReset()
if let counter = response.responseMessage?.id {
nextMessageCounter = Int(counter)
}
}
save(historyItem: response)
guard isFirstTry, hasSecondTry else {
return
}
DispatchQueue.main.async {
sendMessage(from: deviceId, using: keys, isFirstTry: false)
}
}
}
private func preventStateReset() {
stateResetTimer?.invalidate()
stateResetTimer = nil
}
private func scheduleStateReset() {
stateResetTimer?.invalidate()
stateResetTimer = Timer.scheduledTimer(withTimeInterval: 8.0, repeats: false) { _ in
DispatchQueue.main.async {
resetState()
}
}
}
private func resetState() {
state = keyManager.hasAllKeys ? .ready : .noKeyAvailable
preventStateReset()
}
private func save(historyItem: HistoryItem) {
do {
try history.save(item: historyItem)
} catch {
print("Failed to save item: \(error)")
}
}
}
private func send(count: UInt32, from deviceId: UInt8, using keys: KeySet, to server: Client, over localConnection: Bool, while compensatingTime: Bool, localAddress: String, remoteAddress: String) async -> HistoryItem {
let sentTime = Date()
// Add time to compensate that the device is using daylight savings time
let timeCompensation: UInt32 = compensatingTime ? 3600 : 0
let content = Message.Content(
time: sentTime.timestamp + timeCompensation,
id: count,
device: deviceId)
let message = content.authenticate(using: keys.remote)
print("Sending message \(count)")
let address = localConnection ? localAddress : remoteAddress
let (newState, responseMessage) = await send(message, to: server, using: keys.server, local: localConnection, address: address)
var historyItem = HistoryItem(
sent: message.content,
sentDate: sentTime,
local: localConnection,
response: newState,
responseDate: .now,
responseMessage: responseMessage?.content)
guard let responseMessage else {
return historyItem
}
guard responseMessage.isValid(using: keys.device) else {
historyItem.response = .responseRejected(.invalidAuthentication)
return historyItem
}
return historyItem
}
private func send(_ message: Message, to server: Client, using authToken: Data, local: Bool, address: String) async -> (state: ClientState, response: Message?) {
if local {
return await server.sendMessageOverLocalNetwork(message, server: address)
} else {
return await server.send(message, server: address, authToken: authToken)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(didLaunchFromComplication: .constant(false))
.environmentObject(KeyManagement())
.environmentObject(HistoryManager())
}
}