Sesame-iOS/Sesame/ContentView.swift
2022-05-01 14:07:43 +02:00

239 lines
7.7 KiB
Swift

import SwiftUI
import CryptoKit
let server = Client(server: URL(string: "https://christophhagen.de/sesame/")!)
struct ContentView: View {
@AppStorage("counter")
var nextMessageCounter: Int = 0
@State
var keyManager = KeyManagement()
@State
var state: ClientState = .noKeyAvailable
@State
private var timer: Timer?
@State
private var hasActiveRequest = false
@State
private var responseTime: Date? = nil
@State
private var showKeySheet = false
@State
private var showHistorySheet = false
var isPerformingRequests: Bool {
hasActiveRequest ||
state == .waitingForResponse
}
var buttonBackground: Color {
state.allowsAction ?
.white.opacity(0.2) :
.black.opacity(0.2)
}
let buttonBorderWidth: CGFloat = 3
var buttonColor: Color {
state.allowsAction ? .white : .gray
}
private let buttonWidth: CGFloat = 250
private let smallButtonHeight: CGFloat = 50
private let smallButtonWidth: CGFloat = 120
private let smallButtonBorderWidth: CGFloat = 1
var body: some View {
GeometryReader { geo in
VStack(spacing: 20) {
HStack {
Button("History", action: { showHistorySheet = true })
.frame(width: smallButtonWidth,
height: smallButtonHeight)
.background(.white.opacity(0.2))
.cornerRadius(smallButtonHeight / 2)
.overlay(RoundedRectangle(cornerRadius: smallButtonHeight / 2).stroke(lineWidth: smallButtonBorderWidth).foregroundColor(.white))
.foregroundColor(.white)
.font(.title2)
.padding()
Spacer()
Button("Keys", action: { showKeySheet = true })
.frame(width: smallButtonWidth,
height: smallButtonHeight)
.background(.white.opacity(0.2))
.cornerRadius(smallButtonHeight / 2)
.overlay(RoundedRectangle(cornerRadius: smallButtonHeight / 2).stroke(lineWidth: smallButtonBorderWidth).foregroundColor(.white))
.foregroundColor(.white)
.font(.title2)
.padding()
}
Spacer()
if state.requiresDescription {
Text(state.description)
.padding()
}
Button(state.actionText, action: mainButtonPressed)
.frame(width: buttonWidth,
height: buttonWidth)
.background(buttonBackground)
.cornerRadius(buttonWidth / 2)
.overlay(RoundedRectangle(cornerRadius: buttonWidth / 2).stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor))
.foregroundColor(buttonColor)
.font(.title)
.disabled(!state.allowsAction)
.padding(.bottom, (geo.size.width-buttonWidth) / 2)
}
.background(state.color)
.onAppear {
if keyManager.hasAllKeys {
state = .requestingStatus
}
startRegularStatusUpdates()
}
.onDisappear {
endRegularStatusUpdates()
}
.frame(width: geo.size.width, height: geo.size.height)
.animation(.easeInOut, value: state.color)
.sheet(isPresented: $showKeySheet) {
KeyView(keyManager: $keyManager)
}
}
}
func mainButtonPressed() {
guard let key = keyManager.get(.remoteKey),
let token = keyManager.get(.authToken)?.data else {
return
}
let count = UInt32(nextMessageCounter)
let now = Date()
let content = Message.Content(
time: now.timestamp,
id: count)
let message = content.authenticate(using: key)
state = .waitingForResponse
print("Sending message \(count)")
Task {
let (newState, message) = await server.send(message, authToken: token)
responseTime = now
state = newState
if let message = message {
processResponse(message, sendTime: now)
}
}
}
private func processResponse(_ message: Message, sendTime: Date) {
guard let key = keyManager.get(.deviceKey) else {
return
}
guard message.isValid(using: key) else {
return
}
nextMessageCounter = Int(message.content.id)
print("Next counter is \(message.content.id)")
let now = Date()
let total = now.timeIntervalSince(sendTime)
print("Total time: \(Int(total * 1000)) ms")
let deviceTime = Date(timestamp: message.content.time)
let time1 = deviceTime.timeIntervalSince(sendTime)
let time2 = now.timeIntervalSince(deviceTime)
if time1 < 0 {
print("Device time behind by at least \(Int(-time1 * 1000)) ms")
print("Device: \(deviceTime)")
print("Remote: \(now)")
} else if time2 < 0 {
print("Device time ahead by at least \(Int(-time2 * 1000)) ms")
print("Device: \(deviceTime)")
print("Remote: \(now)")
} else {
print("Device time synchronized")
}
}
private func startRegularStatusUpdates() {
guard timer == nil else {
return
}
DispatchQueue.main.async {
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: checkDeviceStatus)
timer!.fire()
}
}
private func endRegularStatusUpdates() {
timer?.invalidate()
timer = nil
}
func checkDeviceStatus(_ timer: Timer) {
guard let authToken = keyManager.get(.authToken) else {
return
}
guard !hasActiveRequest else {
return
}
hasActiveRequest = true
print("Checking device status")
Task {
let newState = await server.deviceStatus(authToken: authToken.data)
hasActiveRequest = false
switch state {
case .noKeyAvailable:
return
case .requestingStatus, .deviceNotAvailable, .ready:
state = newState
case .waitingForResponse:
return
case .messageRejected, .openSesame, .internalError:
guard let time = responseTime else {
state = newState
return
}
responseTime = nil
// Wait at least 5 seconds after these states have been reached before changing the
let elapsed = Date.now.timeIntervalSince(time)
guard elapsed < 5 else {
state = newState
return
}
let secondsToWait = Int(elapsed.rounded(.up))
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(secondsToWait)) {
state = newState
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewDevice("iPhone 8")
}
}
extension Date {
var timestamp: UInt32 {
UInt32(timeIntervalSince1970.rounded())
}
init(timestamp: UInt32) {
self.init(timeIntervalSince1970: TimeInterval(timestamp))
}
}