Files
Gaze/Gaze/Services/EnforceModeService.swift
2026-01-16 01:07:35 -05:00

236 lines
7.3 KiB
Swift

//
// EnforceModeService.swift
// Gaze
//
// Created by Mike Freno on 1/13/26.
//
import Combine
import Foundation
@MainActor
class EnforceModeService: ObservableObject {
static let shared = EnforceModeService()
@Published var isEnforceModeEnabled = false
@Published var isCameraActive = false
@Published var userCompliedWithBreak = false
@Published var isTestMode = false
private var settingsManager: SettingsManager
private var eyeTrackingService: EyeTrackingService
private var timerEngine: TimerEngine?
private var cancellables = Set<AnyCancellable>()
private var faceDetectionTimer: Timer?
private var lastFaceDetectionTime: Date = Date.distantPast
private let faceDetectionTimeout: TimeInterval = 5.0 // 5 seconds to consider person lost
private init() {
self.settingsManager = SettingsManager.shared
self.eyeTrackingService = EyeTrackingService.shared
setupObservers()
initializeEnforceModeState()
}
private func setupObservers() {
eyeTrackingService.$userLookingAtScreen
.sink { [weak self] lookingAtScreen in
self?.handleGazeChange(lookingAtScreen: lookingAtScreen)
}
.store(in: &cancellables)
// Observe face detection changes to track person presence
eyeTrackingService.$faceDetected
.sink { [weak self] faceDetected in
self?.handleFaceDetectionChange(faceDetected: faceDetected)
}
.store(in: &cancellables)
}
private func initializeEnforceModeState() {
let cameraService = CameraAccessService.shared
let settingsEnabled = settingsManager.settings.enforcementMode
// If settings say it's enabled AND camera is authorized, mark as enabled
if settingsEnabled && cameraService.isCameraAuthorized {
isEnforceModeEnabled = true
logDebug("✓ Enforce mode initialized as enabled (camera authorized)")
} else {
isEnforceModeEnabled = false
logDebug("🔒 Enforce mode initialized as disabled")
}
}
func enableEnforceMode() async {
logDebug("🔒 enableEnforceMode called")
guard !isEnforceModeEnabled else {
logError("⚠️ Enforce mode already enabled")
return
}
let cameraService = CameraAccessService.shared
if !cameraService.isCameraAuthorized {
do {
logDebug("🔒 Requesting camera permission...")
try await cameraService.requestCameraAccess()
} catch {
logError("⚠️ Failed to get camera permission: \(error.localizedDescription)")
return
}
}
guard cameraService.isCameraAuthorized else {
logError("❌ Camera permission denied")
return
}
isEnforceModeEnabled = true
logDebug("✓ Enforce mode enabled (camera will activate before lookaway reminders)")
}
func disableEnforceMode() {
guard isEnforceModeEnabled else { return }
stopCamera()
isEnforceModeEnabled = false
userCompliedWithBreak = false
logDebug("✓ Enforce mode disabled")
}
func setTimerEngine(_ engine: TimerEngine) {
self.timerEngine = engine
}
func shouldEnforceBreak(for timerIdentifier: TimerIdentifier) -> Bool {
guard isEnforceModeEnabled else { return false }
guard settingsManager.settings.enforcementMode else { return false }
switch timerIdentifier {
case .builtIn(let type):
return type == .lookAway
case .user:
return false
}
}
func startCameraForLookawayTimer(secondsRemaining: Int) async {
guard isEnforceModeEnabled else { return }
guard !isCameraActive else { return }
logDebug("👁️ Starting camera for lookaway reminder (T-\(secondsRemaining)s)")
do {
try await eyeTrackingService.startEyeTracking()
isCameraActive = true
lastFaceDetectionTime = Date() // Reset grace period
startFaceDetectionTimer()
logDebug("✓ Camera active")
} catch {
logError("⚠️ Failed to start camera: \(error.localizedDescription)")
}
}
func stopCamera() {
guard isCameraActive else { return }
logDebug("👁️ Stopping camera")
eyeTrackingService.stopEyeTracking()
isCameraActive = false
userCompliedWithBreak = false
stopFaceDetectionTimer()
}
func checkUserCompliance() {
guard isCameraActive else {
userCompliedWithBreak = false
return
}
let lookingAway = !eyeTrackingService.userLookingAtScreen
userCompliedWithBreak = lookingAway
}
private func handleGazeChange(lookingAtScreen: Bool) {
guard isCameraActive else { return }
checkUserCompliance()
}
private func handleFaceDetectionChange(faceDetected: Bool) {
// Update the last face detection time only when a face is actively detected
if faceDetected {
lastFaceDetectionTime = Date()
}
}
private func startFaceDetectionTimer() {
stopFaceDetectionTimer()
// Check every 1 second
faceDetectionTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) {
[weak self] _ in
Task { @MainActor [weak self] in
self?.checkFaceDetectionTimeout()
}
}
}
private func stopFaceDetectionTimer() {
faceDetectionTimer?.invalidate()
faceDetectionTimer = nil
}
private func checkFaceDetectionTimeout() {
guard isEnforceModeEnabled && isCameraActive else {
stopFaceDetectionTimer()
return
}
let timeSinceLastDetection = Date().timeIntervalSince(lastFaceDetectionTime)
// If person has not been detected for too long, temporarily disable enforce mode
if timeSinceLastDetection > faceDetectionTimeout {
logDebug(
"⏰ Person not detected for \(faceDetectionTimeout)s. Temporarily disabling enforce mode."
)
disableEnforceMode()
}
}
func handleReminderDismissed() {
// Stop camera when reminder is dismissed, but also check if we should disable enforce mode entirely
// This helps in case a user closes settings window while a reminder is active
if isCameraActive {
stopCamera()
}
}
func startTestMode() async {
guard isEnforceModeEnabled else { return }
guard !isCameraActive else { return }
logDebug("🧪 Starting test mode")
isTestMode = true
do {
try await eyeTrackingService.startEyeTracking()
isCameraActive = true
lastFaceDetectionTime = Date() // Reset grace period
startFaceDetectionTimer()
logDebug("✓ Test mode camera active")
} catch {
logError("⚠️ Failed to start test mode camera: \(error.localizedDescription)")
isTestMode = false
}
}
func stopTestMode() {
guard isTestMode else { return }
logDebug("🧪 Stopping test mode")
stopCamera()
isTestMode = false
}
}