chkpt
This commit is contained in:
@@ -1,97 +0,0 @@
|
|||||||
//
|
|
||||||
// EnforceCameraController.swift
|
|
||||||
// Gaze
|
|
||||||
//
|
|
||||||
// Manages camera lifecycle for enforce mode sessions.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Combine
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
protocol EnforceCameraControllerDelegate: AnyObject {
|
|
||||||
func cameraControllerDidTimeout(_ controller: EnforceCameraController)
|
|
||||||
func cameraController(_ controller: EnforceCameraController, didUpdateLookingAtScreen: Bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
final class EnforceCameraController: ObservableObject {
|
|
||||||
@Published private(set) var isCameraActive = false
|
|
||||||
@Published private(set) var lastFaceDetectionTime: Date = .distantPast
|
|
||||||
|
|
||||||
weak var delegate: EnforceCameraControllerDelegate?
|
|
||||||
|
|
||||||
private let eyeTrackingService: EyeTrackingService
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
|
||||||
private var faceDetectionTimer: Timer?
|
|
||||||
var faceDetectionTimeout: TimeInterval = 5.0
|
|
||||||
|
|
||||||
init(eyeTrackingService: EyeTrackingService) {
|
|
||||||
self.eyeTrackingService = eyeTrackingService
|
|
||||||
setupObservers()
|
|
||||||
}
|
|
||||||
|
|
||||||
func startCamera() async throws {
|
|
||||||
guard !isCameraActive else { return }
|
|
||||||
try await eyeTrackingService.startEyeTracking()
|
|
||||||
isCameraActive = true
|
|
||||||
lastFaceDetectionTime = Date()
|
|
||||||
startFaceDetectionTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopCamera() {
|
|
||||||
guard isCameraActive else { return }
|
|
||||||
eyeTrackingService.stopEyeTracking()
|
|
||||||
isCameraActive = false
|
|
||||||
stopFaceDetectionTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
func resetFaceDetectionTimer() {
|
|
||||||
lastFaceDetectionTime = Date()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupObservers() {
|
|
||||||
eyeTrackingService.$userLookingAtScreen
|
|
||||||
.sink { [weak self] lookingAtScreen in
|
|
||||||
guard let self else { return }
|
|
||||||
self.delegate?.cameraController(self, didUpdateLookingAtScreen: lookingAtScreen)
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
eyeTrackingService.$faceDetected
|
|
||||||
.sink { [weak self] faceDetected in
|
|
||||||
guard let self else { return }
|
|
||||||
if faceDetected {
|
|
||||||
self.lastFaceDetectionTime = Date()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startFaceDetectionTimer() {
|
|
||||||
stopFaceDetectionTimer()
|
|
||||||
|
|
||||||
faceDetectionTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
|
||||||
Task { @MainActor [weak self] in
|
|
||||||
self?.checkFaceDetectionTimeout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private func stopFaceDetectionTimer() {
|
|
||||||
faceDetectionTimer?.invalidate()
|
|
||||||
faceDetectionTimer = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func checkFaceDetectionTimeout() {
|
|
||||||
guard isCameraActive else {
|
|
||||||
stopFaceDetectionTimer()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeSinceLastDetection = Date().timeIntervalSince(lastFaceDetectionTime)
|
|
||||||
if timeSinceLastDetection > faceDetectionTimeout {
|
|
||||||
delegate?.cameraControllerDidTimeout(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
//
|
|
||||||
// EnforcePolicyEvaluator.swift
|
|
||||||
// Gaze
|
|
||||||
//
|
|
||||||
// Policy evaluation for enforce mode behavior.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
enum ComplianceResult {
|
|
||||||
case compliant
|
|
||||||
case notCompliant
|
|
||||||
case faceNotDetected
|
|
||||||
}
|
|
||||||
|
|
||||||
final class EnforcePolicyEvaluator {
|
|
||||||
private let settingsProvider: any SettingsProviding
|
|
||||||
|
|
||||||
init(settingsProvider: any SettingsProviding) {
|
|
||||||
self.settingsProvider = settingsProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
var isEnforcementEnabled: Bool {
|
|
||||||
settingsProvider.isTimerEnabled(for: .lookAway)
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldEnforce(timerIdentifier: TimerIdentifier) -> Bool {
|
|
||||||
guard isEnforcementEnabled else { return false }
|
|
||||||
|
|
||||||
switch timerIdentifier {
|
|
||||||
case .builtIn(let type):
|
|
||||||
return type == .lookAway
|
|
||||||
case .user:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldPreActivateCamera(
|
|
||||||
timerIdentifier: TimerIdentifier,
|
|
||||||
secondsRemaining: Int
|
|
||||||
) -> Bool {
|
|
||||||
guard secondsRemaining <= 3 else { return false }
|
|
||||||
return shouldEnforce(timerIdentifier: timerIdentifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
func evaluateCompliance(
|
|
||||||
isLookingAtScreen: Bool,
|
|
||||||
faceDetected: Bool
|
|
||||||
) -> ComplianceResult {
|
|
||||||
guard faceDetected else { return .faceNotDetected }
|
|
||||||
return isLookingAtScreen ? .notCompliant : .compliant
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,139 @@
|
|||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
enum ComplianceResult {
|
||||||
|
case compliant
|
||||||
|
case notCompliant
|
||||||
|
case faceNotDetected
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol EnforceCameraControllerDelegate: AnyObject {
|
||||||
|
func cameraControllerDidTimeout(_ controller: EnforceCameraController)
|
||||||
|
func cameraController(_ controller: EnforceCameraController, didUpdateLookingAtScreen: Bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class EnforcePolicyEvaluator {
|
||||||
|
private let settingsProvider: any SettingsProviding
|
||||||
|
|
||||||
|
init(settingsProvider: any SettingsProviding) {
|
||||||
|
self.settingsProvider = settingsProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEnforcementEnabled: Bool {
|
||||||
|
settingsProvider.isTimerEnabled(for: .lookAway)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldEnforce(timerIdentifier: TimerIdentifier) -> Bool {
|
||||||
|
guard isEnforcementEnabled else { return false }
|
||||||
|
|
||||||
|
switch timerIdentifier {
|
||||||
|
case .builtIn(let type):
|
||||||
|
return type == .lookAway
|
||||||
|
case .user:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldPreActivateCamera(
|
||||||
|
timerIdentifier: TimerIdentifier,
|
||||||
|
secondsRemaining: Int
|
||||||
|
) -> Bool {
|
||||||
|
guard secondsRemaining <= 3 else { return false }
|
||||||
|
return shouldEnforce(timerIdentifier: timerIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluateCompliance(
|
||||||
|
isLookingAtScreen: Bool,
|
||||||
|
faceDetected: Bool
|
||||||
|
) -> ComplianceResult {
|
||||||
|
guard faceDetected else { return .faceNotDetected }
|
||||||
|
return isLookingAtScreen ? .notCompliant : .compliant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class EnforceCameraController: ObservableObject {
|
||||||
|
@Published private(set) var isCameraActive = false
|
||||||
|
@Published private(set) var lastFaceDetectionTime: Date = .distantPast
|
||||||
|
|
||||||
|
weak var delegate: EnforceCameraControllerDelegate?
|
||||||
|
|
||||||
|
private let eyeTrackingService: EyeTrackingService
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private var faceDetectionTimer: Timer?
|
||||||
|
var faceDetectionTimeout: TimeInterval = 5.0
|
||||||
|
|
||||||
|
init(eyeTrackingService: EyeTrackingService) {
|
||||||
|
self.eyeTrackingService = eyeTrackingService
|
||||||
|
setupObservers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func startCamera() async throws {
|
||||||
|
guard !isCameraActive else { return }
|
||||||
|
try await eyeTrackingService.startEyeTracking()
|
||||||
|
isCameraActive = true
|
||||||
|
lastFaceDetectionTime = Date()
|
||||||
|
startFaceDetectionTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopCamera() {
|
||||||
|
guard isCameraActive else { return }
|
||||||
|
eyeTrackingService.stopEyeTracking()
|
||||||
|
isCameraActive = false
|
||||||
|
stopFaceDetectionTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetFaceDetectionTimer() {
|
||||||
|
lastFaceDetectionTime = Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupObservers() {
|
||||||
|
eyeTrackingService.$userLookingAtScreen
|
||||||
|
.sink { [weak self] lookingAtScreen in
|
||||||
|
guard let self else { return }
|
||||||
|
self.delegate?.cameraController(self, didUpdateLookingAtScreen: lookingAtScreen)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
eyeTrackingService.$faceDetected
|
||||||
|
.sink { [weak self] faceDetected in
|
||||||
|
guard let self else { return }
|
||||||
|
if faceDetected {
|
||||||
|
self.lastFaceDetectionTime = Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startFaceDetectionTimer() {
|
||||||
|
stopFaceDetectionTimer()
|
||||||
|
|
||||||
|
faceDetectionTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||||||
|
Task { @MainActor [weak self] in
|
||||||
|
self?.checkFaceDetectionTimeout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopFaceDetectionTimer() {
|
||||||
|
faceDetectionTimer?.invalidate()
|
||||||
|
faceDetectionTimer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkFaceDetectionTimeout() {
|
||||||
|
guard isCameraActive else {
|
||||||
|
stopFaceDetectionTimer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeSinceLastDetection = Date().timeIntervalSince(lastFaceDetectionTime)
|
||||||
|
if timeSinceLastDetection > faceDetectionTimeout {
|
||||||
|
delegate?.cameraControllerDidTimeout(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class EnforceModeService: ObservableObject {
|
class EnforceModeService: ObservableObject {
|
||||||
static let shared = EnforceModeService()
|
static let shared = EnforceModeService()
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
//
|
|
||||||
// ReminderManager.swift
|
|
||||||
// Gaze
|
|
||||||
//
|
|
||||||
// Manages reminder triggering and dismissal logic for timers.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Combine
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
class ReminderManager: ObservableObject {
|
|
||||||
@Published var activeReminder: ReminderEvent?
|
|
||||||
|
|
||||||
private let settingsProvider: any SettingsProviding
|
|
||||||
private var enforceModeService: EnforceModeService?
|
|
||||||
private var timerEngine: TimerEngine?
|
|
||||||
|
|
||||||
init(
|
|
||||||
settingsProvider: any SettingsProviding,
|
|
||||||
enforceModeService: EnforceModeService? = nil
|
|
||||||
) {
|
|
||||||
self.settingsProvider = settingsProvider
|
|
||||||
self.enforceModeService = enforceModeService ?? EnforceModeService.shared
|
|
||||||
}
|
|
||||||
|
|
||||||
func setTimerEngine(_ engine: TimerEngine) {
|
|
||||||
self.timerEngine = engine
|
|
||||||
}
|
|
||||||
|
|
||||||
func triggerReminder(for identifier: TimerIdentifier) {
|
|
||||||
// Pause only the timer that triggered
|
|
||||||
timerEngine?.pauseTimer(identifier: identifier)
|
|
||||||
|
|
||||||
// Unified approach to handle all timer types - no more special handling
|
|
||||||
switch identifier {
|
|
||||||
case .builtIn(let type):
|
|
||||||
switch type {
|
|
||||||
case .lookAway:
|
|
||||||
activeReminder = .lookAwayTriggered(
|
|
||||||
countdownSeconds: settingsProvider.timerIntervalMinutes(for: .lookAway) * 60)
|
|
||||||
case .blink:
|
|
||||||
activeReminder = .blinkTriggered
|
|
||||||
case .posture:
|
|
||||||
activeReminder = .postureTriggered
|
|
||||||
}
|
|
||||||
case .user(let id):
|
|
||||||
if let userTimer = settingsProvider.settings.userTimers.first(where: { $0.id == id }) {
|
|
||||||
activeReminder = .userTimerTriggered(userTimer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dismissReminder() {
|
|
||||||
guard let reminder = activeReminder else { return }
|
|
||||||
activeReminder = nil
|
|
||||||
|
|
||||||
let identifier = reminder.identifier
|
|
||||||
timerEngine?.skipNext(identifier: identifier)
|
|
||||||
timerEngine?.resumeTimer(identifier: identifier)
|
|
||||||
|
|
||||||
enforceModeService?.handleReminderDismissed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
//
|
|
||||||
// TimerManager.swift
|
|
||||||
// Gaze
|
|
||||||
//
|
|
||||||
// Manages timer creation, state updates, and lifecycle operations.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Combine
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
class TimerManager: ObservableObject {
|
|
||||||
@Published var timerStates: [TimerIdentifier: TimerState] = [:]
|
|
||||||
|
|
||||||
private let settingsProvider: any SettingsProviding
|
|
||||||
private var timerSubscription: AnyCancellable?
|
|
||||||
private let timeProvider: TimeProviding
|
|
||||||
|
|
||||||
init(
|
|
||||||
settingsManager: any SettingsProviding,
|
|
||||||
timeProvider: TimeProviding
|
|
||||||
) {
|
|
||||||
self.settingsProvider = settingsManager
|
|
||||||
self.timeProvider = timeProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
func start() {
|
|
||||||
// If timers are already running, just update configurations without resetting
|
|
||||||
if timerSubscription != nil {
|
|
||||||
updateConfigurations()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial start - create all timer states
|
|
||||||
stop()
|
|
||||||
|
|
||||||
var newStates: [TimerIdentifier: TimerState] = [:]
|
|
||||||
|
|
||||||
// Add built-in timers (using unified approach)
|
|
||||||
for timerType in TimerType.allCases {
|
|
||||||
let intervalSeconds = settingsProvider.timerIntervalMinutes(for: timerType) * 60
|
|
||||||
if settingsProvider.isTimerEnabled(for: timerType) {
|
|
||||||
let identifier = TimerIdentifier.builtIn(timerType)
|
|
||||||
newStates[identifier] = TimerStateBuilder.make(
|
|
||||||
identifier: identifier,
|
|
||||||
intervalSeconds: intervalSeconds
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add user timers (using unified approach)
|
|
||||||
for userTimer in settingsProvider.settings.userTimers where userTimer.enabled {
|
|
||||||
let identifier = TimerIdentifier.user(id: userTimer.id)
|
|
||||||
newStates[identifier] = TimerStateBuilder.make(
|
|
||||||
identifier: identifier,
|
|
||||||
intervalSeconds: userTimer.intervalMinutes * 60
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign the entire dictionary at once to trigger @Published
|
|
||||||
timerStates = newStates
|
|
||||||
|
|
||||||
timerSubscription = Timer.publish(every: 1.0, on: .main, in: .common)
|
|
||||||
.autoconnect()
|
|
||||||
.sink { [weak self] _ in
|
|
||||||
Task { @MainActor in
|
|
||||||
self?.handleTick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stop() {
|
|
||||||
timerSubscription?.cancel()
|
|
||||||
timerSubscription = nil
|
|
||||||
timerStates.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateConfigurations() {
|
|
||||||
// Update configurations from settings
|
|
||||||
var newStates: [TimerIdentifier: TimerState] = [:]
|
|
||||||
|
|
||||||
// Update built-in timers (using unified approach)
|
|
||||||
for timerType in TimerType.allCases {
|
|
||||||
let intervalSeconds = settingsProvider.timerIntervalMinutes(for: timerType) * 60
|
|
||||||
let identifier = TimerIdentifier.builtIn(timerType)
|
|
||||||
|
|
||||||
if settingsProvider.isTimerEnabled(for: timerType) {
|
|
||||||
if let existingState = timerStates[identifier] {
|
|
||||||
// Timer exists - check if interval changed
|
|
||||||
if existingState.originalIntervalSeconds != intervalSeconds {
|
|
||||||
// Interval changed - reset with new interval
|
|
||||||
var updatedState = existingState
|
|
||||||
updatedState.reset(intervalSeconds: intervalSeconds, keepPaused: true)
|
|
||||||
newStates[identifier] = updatedState
|
|
||||||
} else {
|
|
||||||
// Interval unchanged - keep existing state
|
|
||||||
newStates[identifier] = existingState
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Timer was just enabled - create new state
|
|
||||||
newStates[identifier] = TimerStateBuilder.make(
|
|
||||||
identifier: identifier,
|
|
||||||
intervalSeconds: intervalSeconds
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If timer is disabled, it will be removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update user timers (using unified approach)
|
|
||||||
for userTimer in settingsProvider.settings.userTimers {
|
|
||||||
let identifier = TimerIdentifier.user(id: userTimer.id)
|
|
||||||
let newIntervalSeconds = userTimer.intervalMinutes * 60
|
|
||||||
|
|
||||||
if userTimer.enabled {
|
|
||||||
if let existingState = timerStates[identifier] {
|
|
||||||
// Check if interval changed
|
|
||||||
if existingState.originalIntervalSeconds != newIntervalSeconds {
|
|
||||||
// Interval changed - reset with new interval
|
|
||||||
var updatedState = existingState
|
|
||||||
updatedState.reset(intervalSeconds: newIntervalSeconds, keepPaused: true)
|
|
||||||
newStates[identifier] = updatedState
|
|
||||||
} else {
|
|
||||||
// Interval unchanged - keep existing state
|
|
||||||
newStates[identifier] = existingState
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// New timer - create state
|
|
||||||
newStates[identifier] = TimerStateBuilder.make(
|
|
||||||
identifier: identifier,
|
|
||||||
intervalSeconds: newIntervalSeconds
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If timer is disabled, it will be removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign the entire dictionary at once to trigger @Published
|
|
||||||
timerStates = newStates
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleTick() {
|
|
||||||
for (identifier, state) in timerStates {
|
|
||||||
guard !state.isPaused else { continue }
|
|
||||||
guard state.isActive else { continue }
|
|
||||||
|
|
||||||
if state.targetDate(using: timeProvider) < timeProvider.now() - 3.0 {
|
|
||||||
// Timer has expired but with some grace period
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
timerStates[identifier]?.remainingSeconds -= 1
|
|
||||||
|
|
||||||
if let updatedState = timerStates[identifier] {
|
|
||||||
// Update remaining seconds for the timer
|
|
||||||
if updatedState.remainingSeconds <= 0 {
|
|
||||||
// This would normally trigger a reminder in a full implementation,
|
|
||||||
// but we're decomposing it to separate components
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func pause() {
|
|
||||||
for (id, var state) in timerStates {
|
|
||||||
state.pauseReasons.insert(.manual)
|
|
||||||
state.isPaused = true
|
|
||||||
timerStates[id] = state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func resume() {
|
|
||||||
for (id, var state) in timerStates {
|
|
||||||
state.pauseReasons.remove(.manual)
|
|
||||||
state.isPaused = !state.pauseReasons.isEmpty
|
|
||||||
timerStates[id] = state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func pauseTimer(identifier: TimerIdentifier) {
|
|
||||||
guard var state = timerStates[identifier] else { return }
|
|
||||||
state.pauseReasons.insert(.manual)
|
|
||||||
state.isPaused = true
|
|
||||||
timerStates[identifier] = state
|
|
||||||
}
|
|
||||||
|
|
||||||
func resumeTimer(identifier: TimerIdentifier) {
|
|
||||||
guard var state = timerStates[identifier] else { return }
|
|
||||||
state.pauseReasons.remove(.manual)
|
|
||||||
state.isPaused = !state.pauseReasons.isEmpty
|
|
||||||
timerStates[identifier] = state
|
|
||||||
}
|
|
||||||
|
|
||||||
func skipNext(identifier: TimerIdentifier) {
|
|
||||||
guard let state = timerStates[identifier] else { return }
|
|
||||||
|
|
||||||
// Unified approach to get interval - no more separate handling for user timers
|
|
||||||
let intervalSeconds = getTimerInterval(for: identifier)
|
|
||||||
|
|
||||||
var updatedState = state
|
|
||||||
updatedState.reset(intervalSeconds: intervalSeconds, keepPaused: true)
|
|
||||||
timerStates[identifier] = updatedState
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unified way to get interval for any timer type
|
|
||||||
private func getTimerInterval(for identifier: TimerIdentifier) -> Int {
|
|
||||||
switch identifier {
|
|
||||||
case .builtIn(let type):
|
|
||||||
return settingsProvider.timerIntervalMinutes(for: type) * 60
|
|
||||||
case .user(let id):
|
|
||||||
guard let userTimer = settingsProvider.settings.userTimers.first(where: { $0.id == id }) else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return userTimer.intervalMinutes * 60
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTimeRemaining(for identifier: TimerIdentifier) -> TimeInterval {
|
|
||||||
timerStates[identifier]?.remainingDuration ?? 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFormattedTimeRemaining(for identifier: TimerIdentifier) -> String {
|
|
||||||
return getTimeRemaining(for: identifier).formatAsTimerDurationFull()
|
|
||||||
}
|
|
||||||
|
|
||||||
func isTimerPaused(_ identifier: TimerIdentifier) -> Bool {
|
|
||||||
return timerStates[identifier]?.isPaused ?? true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
217
GazeTests/Services/EnforcePolicyEvaluatorTests.swift
Normal file
217
GazeTests/Services/EnforcePolicyEvaluatorTests.swift
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
//
|
||||||
|
// EnforcePolicyEvaluatorTests.swift
|
||||||
|
// GazeTests
|
||||||
|
//
|
||||||
|
// Unit tests for EnforcePolicyEvaluator (now nested in EnforceModeService).
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import Gaze
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class EnforcePolicyEvaluatorTests: XCTestCase {
|
||||||
|
|
||||||
|
var evaluator: EnforcePolicyEvaluator!
|
||||||
|
var mockSettings: EnhancedMockSettingsManager!
|
||||||
|
|
||||||
|
override func setUp() async throws {
|
||||||
|
mockSettings = EnhancedMockSettingsManager(settings: .defaults)
|
||||||
|
evaluator = EnforcePolicyEvaluator(settingsProvider: mockSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDown() async throws {
|
||||||
|
evaluator = nil
|
||||||
|
mockSettings = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Initialization Tests
|
||||||
|
|
||||||
|
func testInitialization() {
|
||||||
|
XCTAssertNotNil(evaluator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInitializationWithSettingsProvider() {
|
||||||
|
let newSettings = EnhancedMockSettingsManager(settings: AppSettings.defaults)
|
||||||
|
let newEvaluator = EnforcePolicyEvaluator(settingsProvider: newSettings)
|
||||||
|
XCTAssertNotNil(newEvaluator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Enforcement Enabled Tests
|
||||||
|
|
||||||
|
func testIsEnforcementEnabledWhenLookAwayDisabled() {
|
||||||
|
mockSettings.updateTimerEnabled(for: .lookAway, enabled: false)
|
||||||
|
|
||||||
|
let isEnabled = evaluator.isEnforcementEnabled
|
||||||
|
|
||||||
|
XCTAssertFalse(isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsEnforcementEnabledWhenLookAwayEnabled() {
|
||||||
|
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||||
|
|
||||||
|
let isEnabled = evaluator.isEnforcementEnabled
|
||||||
|
|
||||||
|
XCTAssertTrue(isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Should Enforce Tests
|
||||||
|
|
||||||
|
func testShouldEnforceWhenLookAwayEnabled() {
|
||||||
|
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||||
|
|
||||||
|
let shouldEnforce = evaluator.shouldEnforce(timerIdentifier: .builtIn(.lookAway))
|
||||||
|
|
||||||
|
XCTAssertTrue(shouldEnforce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testShouldEnforceWhenLookAwayDisabled() {
|
||||||
|
mockSettings.updateTimerEnabled(for: .lookAway, enabled: false)
|
||||||
|
|
||||||
|
let shouldEnforce = evaluator.shouldEnforce(timerIdentifier: .builtIn(.lookAway))
|
||||||
|
|
||||||
|
XCTAssertFalse(shouldEnforce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testShouldEnforceUserTimerNever() {
|
||||||
|
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||||
|
|
||||||
|
let shouldEnforce = evaluator.shouldEnforce(timerIdentifier: .user)
|
||||||
|
|
||||||
|
XCTAssertFalse(shouldEnforce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testShouldEnforceBuiltInPostureTimerNever() {
|
||||||
|
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||||
|
|
||||||
|
let shouldEnforce = evaluator.shouldEnforce(timerIdentifier: .builtIn(.posture))
|
||||||
|
|
||||||
|
XCTAssertFalse(shouldEnforce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testShouldEnforceBuiltInBlinkTimerNever() {
|
||||||
|
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||||
|
|
||||||
|
let shouldEnforce = evaluator.shouldEnforce(timerIdentifier: .builtIn(.blink))
|
||||||
|
|
||||||
|
XCTAssertFalse(shouldEnforce)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Pre-activate Camera Tests
|
||||||
|
|
||||||
|
func testShouldPreActivateCameraWhenTimerDisabled() {
|
||||||
|
mockSettings.updateTimerEnabled(for: .lookAway, enabled: false)
|
||||||
|
|
||||||
|
let shouldPreActivate = evaluator.shouldPreActivateCamera(
|
||||||
|
timerIdentifier: .builtIn(.lookAway),
|
||||||
|
secondsRemaining: 3
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertFalse(shouldPreActivate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testShouldPreActivateCameraWhenSecondsRemainingTooHigh() {
|
||||||
|
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||||
|
|
||||||
|
let shouldPreActivate = evaluator.shouldPreActivateCamera(
|
||||||
|
timerIdentifier: .builtIn(.lookAway),
|
||||||
|
secondsRemaining: 5
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertFalse(shouldPreActivate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testShouldPreActivateCameraWhenAllConditionsMet() {
|
||||||
|
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||||
|
|
||||||
|
let shouldPreActivate = evaluator.shouldPreActivateCamera(
|
||||||
|
timerIdentifier: .builtIn(.lookAway),
|
||||||
|
secondsRemaining: 2
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertTrue(shouldPreActivate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testShouldPreActivateCameraForUserTimerNever() {
|
||||||
|
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||||
|
|
||||||
|
let shouldPreActivate = evaluator.shouldPreActivateCamera(
|
||||||
|
timerIdentifier: .user,
|
||||||
|
secondsRemaining: 1
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertFalse(shouldPreActivate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Compliance Evaluation Tests
|
||||||
|
|
||||||
|
func testEvaluateComplianceWhenLookingAtScreenAndFaceDetected() {
|
||||||
|
let result = evaluator.evaluateCompliance(
|
||||||
|
isLookingAtScreen: true,
|
||||||
|
faceDetected: true
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(result, .notCompliant)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEvaluateComplianceWhenNotLookingAtScreenAndFaceDetected() {
|
||||||
|
let result = evaluator.evaluateCompliance(
|
||||||
|
isLookingAtScreen: false,
|
||||||
|
faceDetected: true
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(result, .compliant)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEvaluateComplianceWhenFaceNotDetected() {
|
||||||
|
let result = evaluator.evaluateCompliance(
|
||||||
|
isLookingAtScreen: true,
|
||||||
|
faceDetected: false
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(result, .faceNotDetected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEvaluateComplianceWhenFaceNotDetectedAndNotLookingAtScreen() {
|
||||||
|
let result = evaluator.evaluateCompliance(
|
||||||
|
isLookingAtScreen: false,
|
||||||
|
faceDetected: false
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(result, .faceNotDetected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEvaluateComplianceWhenFaceNotDetectedAndNotLookingAtScreen() {
|
||||||
|
// Test edge case - should still return face not detected
|
||||||
|
let result = evaluator.evaluateCompliance(
|
||||||
|
isLookingAtScreen: false,
|
||||||
|
faceDetected: false
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(result, .faceNotDetected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Integration Tests
|
||||||
|
|
||||||
|
func testFullEnforcementFlow() {
|
||||||
|
// Setup: Look away timer enabled
|
||||||
|
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||||
|
|
||||||
|
// Test 1: Check enforcement
|
||||||
|
let shouldEnforce = evaluator.shouldEnforce(timerIdentifier: .builtIn(.lookAway))
|
||||||
|
XCTAssertTrue(shouldEnforce)
|
||||||
|
|
||||||
|
// Test 2: Check pre-activation at 3 seconds
|
||||||
|
let shouldPreActivate = evaluator.shouldPreActivateCamera(
|
||||||
|
timerIdentifier: .builtIn(.lookAway),
|
||||||
|
secondsRemaining: 3
|
||||||
|
)
|
||||||
|
XCTAssertTrue(shouldPreActivate)
|
||||||
|
|
||||||
|
// Test 3: Check compliance when looking at screen
|
||||||
|
let compliance = evaluator.evaluateCompliance(
|
||||||
|
isLookingAtScreen: true,
|
||||||
|
faceDetected: true
|
||||||
|
)
|
||||||
|
XCTAssertEqual(compliance, .notCompliant)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -265,8 +265,8 @@ final class TimerEngineTests: XCTestCase {
|
|||||||
|
|
||||||
systemSleepManager.handleSystemWillSleep()
|
systemSleepManager.handleSystemWillSleep()
|
||||||
|
|
||||||
// States should still exist
|
// States should be cleared
|
||||||
XCTAssertEqual(timerEngine.timerStates.count, statesBefore)
|
XCTAssertEqual(timerEngine.timerStates.count, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSystemSleepManagerHandlesWake() {
|
func testSystemSleepManagerHandlesWake() {
|
||||||
|
|||||||
Reference in New Issue
Block a user