This commit is contained in:
Michael Freno
2026-01-29 16:21:44 -05:00
parent 6e3730849b
commit 817f391305
4 changed files with 207 additions and 448 deletions

View File

@@ -1,63 +0,0 @@
# AppDelegate Responsibilities Assessment
## Overview
This document assesses the responsibilities currently handled by the AppDelegate in the Gaze application, identifying the core functions and potential areas for improvement.
## Current Responsibilities
### 1. Application Lifecycle Management
- Handles `applicationDidFinishLaunching` to initialize app state
- Manages `applicationWillTerminate` for cleanup
- Sets up system lifecycle observers (sleep/wake notifications)
### 2. Service Initialization
- Initializes the TimerEngine
- Sets up smart mode services (FullscreenDetectionService, IdleMonitoringService, UsageTrackingService)
- Configures update manager after onboarding completion
### 3. Settings Management
- Observes settings changes to start/stop timers appropriately
- Handles onboarding state management
- Manages Smart Mode settings observation
### 4. User Interface Management
- Displays onboarding at launch if needed
- Shows reminder windows (overlay and subtle)
- Manages settings and onboarding windows through WindowManager
- Handles menu dismissal logic for proper UI flow
### 5. Timer State Management
- Starts timers when onboarding is complete
- Handles system sleep/wake events
- Observes timer state changes to update UI
## Key Findings
### Positive Aspects:
- Clear separation of concerns with service container pattern
- Dependency injection allows for testing
- Lifecycle management is centralized
- Window management is abstracted through protocol
### Potential Issues:
- AppDelegate is handling too many responsibilities (service coordination, UI management, lifecycle)
- Direct dependency on NSWorkspace notifications instead of using more structured event handling
- Tight coupling between multiple services and the AppDelegate
## Recommendations
1. **Reduce AppDelegate Responsibilities**:
- Move timer state change handling to TimerEngine or a dedicated timer manager
- Extract window management logic into separate components
- Consider delegating system lifecycle handling to dedicated observers
2. **Improve Modularity**:
- Create a dedicated service coordinator that handles inter-service communication
- Implement a more structured event system for state changes instead of direct observing
3. **Enhance Testability**:
- The current dependency injection approach is good, but could be made even more flexible
- Add more granular mocking capabilities for individual services
## Conclusion
While the AppDelegate currently fulfills its role in managing application lifecycle and coordinating services, it's handling too many responsibilities that should ideally be distributed among specialized components. This makes the code harder to test and maintain.

View File

@@ -14,91 +14,57 @@ enum ComplianceResult {
case faceNotDetected
}
protocol EnforceCameraControllerDelegate: AnyObject {
func cameraControllerDidTimeout(_ controller: EnforceCameraController)
func cameraController(_ controller: EnforceCameraController, didUpdateLookingAtScreen: Bool)
}
@MainActor
class EnforceModeService: ObservableObject {
static let shared = EnforceModeService()
final class EnforcePolicyEvaluator {
private let settingsProvider: any SettingsProviding
// MARK: - Published State
init(settingsProvider: any SettingsProviding) {
self.settingsProvider = settingsProvider
@Published var isEnforceModeEnabled = false
@Published var isCameraActive = false
@Published var userCompliedWithBreak = false
@Published var isTestMode = false
// MARK: - Private Properties
private var settingsManager: SettingsManager
private var eyeTrackingService: EyeTrackingService
private var timerEngine: TimerEngine?
private var cancellables = Set<AnyCancellable>()
private var faceDetectionTimer: Timer?
// MARK: - Configuration
private(set) var lastFaceDetectionTime: Date = .distantPast
var faceDetectionTimeout: TimeInterval = 5.0
// MARK: - Initialization
private init() {
self.settingsManager = SettingsManager.shared
self.eyeTrackingService = EyeTrackingService.shared
setupEyeTrackingObservers()
initializeEnforceModeState()
}
var isEnforcementEnabled: Bool {
settingsProvider.isTimerEnabled(for: .lookAway)
}
private func initializeEnforceModeState() {
let cameraService = CameraAccessService.shared
let settingsEnabled = isEnforcementEnabled
func shouldEnforce(timerIdentifier: TimerIdentifier) -> Bool {
guard isEnforcementEnabled else { return false }
switch timerIdentifier {
case .builtIn(let type):
return type == .lookAway
case .user:
return false
if settingsEnabled && cameraService.isCameraAuthorized {
isEnforceModeEnabled = true
logDebug("✓ Enforce mode initialized as enabled (camera authorized)")
} else {
isEnforceModeEnabled = false
logDebug("🔒 Enforce mode initialized as disabled")
}
}
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() {
private func setupEyeTrackingObservers() {
eyeTrackingService.$userLookingAtScreen
.sink { [weak self] lookingAtScreen in
guard let self else { return }
self.delegate?.cameraController(self, didUpdateLookingAtScreen: lookingAtScreen)
.sink { [weak self] _ in
guard let self, self.isCameraActive else { return }
self.checkUserCompliance()
}
.store(in: &cancellables)
@@ -112,70 +78,7 @@ class EnforceCameraController: ObservableObject {
.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
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 let policyEvaluator: EnforcePolicyEvaluator
private let cameraController: EnforceCameraController
private var timerEngine: TimerEngine?
private init() {
self.settingsManager = SettingsManager.shared
self.policyEvaluator = EnforcePolicyEvaluator(settingsProvider: SettingsManager.shared)
self.cameraController = EnforceCameraController(eyeTrackingService: EyeTrackingService.shared)
self.cameraController.delegate = self
initializeEnforceModeState()
}
private func initializeEnforceModeState() {
let cameraService = CameraAccessService.shared
let settingsEnabled = policyEvaluator.isEnforcementEnabled
// 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")
}
}
// MARK: - Enable/Disable
func enableEnforceMode() async {
logDebug("🔒 enableEnforceMode called")
@@ -217,42 +120,87 @@ class EnforceModeService: ObservableObject {
self.timerEngine = engine
}
// MARK: - Policy Evaluation
var isEnforcementEnabled: Bool {
settingsManager.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 shouldEnforceBreak(for timerIdentifier: TimerIdentifier) -> Bool {
guard isEnforceModeEnabled else { return false }
return policyEvaluator.shouldEnforce(timerIdentifier: timerIdentifier)
return shouldEnforce(timerIdentifier: timerIdentifier)
}
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
}
// MARK: - Camera Control
func startCameraForLookawayTimer(secondsRemaining: Int) async {
guard isEnforceModeEnabled else { return }
logDebug("👁️ Starting camera for lookaway reminder (T-\(secondsRemaining)s)")
do {
try await cameraController.startCamera()
isCameraActive = cameraController.isCameraActive
try await startCamera()
logDebug("✓ Camera active")
} catch {
logError("⚠️ Failed to start camera: \(error.localizedDescription)")
}
}
private func startCamera() async throws {
guard !isCameraActive else { return }
try await eyeTrackingService.startEyeTracking()
isCameraActive = true
lastFaceDetectionTime = Date()
startFaceDetectionTimer()
}
func stopCamera() {
guard isCameraActive else { return }
logDebug("👁️ Stopping camera")
cameraController.stopCamera()
eyeTrackingService.stopEyeTracking()
isCameraActive = false
stopFaceDetectionTimer()
userCompliedWithBreak = false
}
// MARK: - Compliance Checking
func checkUserCompliance() {
guard isCameraActive else {
userCompliedWithBreak = false
return
}
let compliance = policyEvaluator.evaluateCompliance(
isLookingAtScreen: EyeTrackingService.shared.userLookingAtScreen,
faceDetected: EyeTrackingService.shared.faceDetected
let compliance = evaluateCompliance(
isLookingAtScreen: eyeTrackingService.userLookingAtScreen,
faceDetected: eyeTrackingService.faceDetected
)
switch compliance {
@@ -266,13 +214,43 @@ class EnforceModeService: ObservableObject {
}
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()
}
}
// MARK: - Face Detection Timer
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 {
logDebug("⏰ Person not detected for \(faceDetectionTimeout)s. Temporarily disabling enforce mode.")
disableEnforceMode()
}
}
// MARK: - Test Mode
func startTestMode() async {
guard isEnforceModeEnabled else { return }
@@ -280,8 +258,7 @@ class EnforceModeService: ObservableObject {
isTestMode = true
do {
try await cameraController.startCamera()
isCameraActive = cameraController.isCameraActive
try await startCamera()
logDebug("✓ Test mode camera active")
} catch {
logError("⚠️ Failed to start test mode camera: \(error.localizedDescription)")
@@ -297,17 +274,3 @@ class EnforceModeService: ObservableObject {
isTestMode = false
}
}
extension EnforceModeService: EnforceCameraControllerDelegate {
func cameraControllerDidTimeout(_ controller: EnforceCameraController) {
logDebug(
"⏰ Person not detected for \(controller.faceDetectionTimeout)s. Temporarily disabling enforce mode."
)
disableEnforceMode()
}
func cameraController(_ controller: EnforceCameraController, didUpdateLookingAtScreen: Bool) {
guard isCameraActive else { return }
checkUserCompliance()
}
}

View File

@@ -70,7 +70,7 @@ final class EnforceModeServiceTests: XCTestCase {
XCTAssertNotNil(service)
}
// MARK: - Should Enforce Break Tests
// MARK: - Should Enforce Tests
func testShouldEnforceBreakWhenDisabled() {
service.disableEnforceMode()
@@ -79,6 +79,79 @@ final class EnforceModeServiceTests: XCTestCase {
XCTAssertFalse(shouldEnforce)
}
func testShouldEnforceLookAwayTimer() {
let shouldEnforce = service.shouldEnforce(timerIdentifier: .builtIn(.lookAway))
// Result depends on settings, but method should not crash
XCTAssertNotNil(shouldEnforce)
}
func testShouldEnforceUserTimerNever() {
let shouldEnforce = service.shouldEnforce(timerIdentifier: .user(id: "test"))
XCTAssertFalse(shouldEnforce)
}
func testShouldEnforceBuiltInPostureTimerNever() {
let shouldEnforce = service.shouldEnforce(timerIdentifier: .builtIn(.posture))
XCTAssertFalse(shouldEnforce)
}
func testShouldEnforceBuiltInBlinkTimerNever() {
let shouldEnforce = service.shouldEnforce(timerIdentifier: .builtIn(.blink))
XCTAssertFalse(shouldEnforce)
}
// MARK: - Pre-activate Camera Tests
func testShouldPreActivateCameraWhenSecondsRemainingTooHigh() {
let shouldPreActivate = service.shouldPreActivateCamera(
timerIdentifier: .builtIn(.lookAway),
secondsRemaining: 5
)
XCTAssertFalse(shouldPreActivate)
}
func testShouldPreActivateCameraForUserTimerNever() {
let shouldPreActivate = service.shouldPreActivateCamera(
timerIdentifier: .user(id: "test"),
secondsRemaining: 1
)
XCTAssertFalse(shouldPreActivate)
}
// MARK: - Compliance Evaluation Tests
func testEvaluateComplianceWhenLookingAtScreenAndFaceDetected() {
let result = service.evaluateCompliance(
isLookingAtScreen: true,
faceDetected: true
)
XCTAssertEqual(result, .notCompliant)
}
func testEvaluateComplianceWhenNotLookingAtScreenAndFaceDetected() {
let result = service.evaluateCompliance(
isLookingAtScreen: false,
faceDetected: true
)
XCTAssertEqual(result, .compliant)
}
func testEvaluateComplianceWhenFaceNotDetected() {
let result = service.evaluateCompliance(
isLookingAtScreen: true,
faceDetected: false
)
XCTAssertEqual(result, .faceNotDetected)
}
func testEvaluateComplianceWhenFaceNotDetectedAndNotLookingAtScreen() {
let result = service.evaluateCompliance(
isLookingAtScreen: false,
faceDetected: false
)
XCTAssertEqual(result, .faceNotDetected)
}
// MARK: - Camera Tests
func testStopCamera() {
@@ -106,9 +179,12 @@ final class EnforceModeServiceTests: XCTestCase {
// MARK: - Test Mode Tests
func testStartTestMode() async {
await service.enableEnforceMode()
await service.startTestMode()
XCTAssertTrue(service.isTestMode)
// Test mode requires enforce mode to be enabled and camera permissions
// Just verify it doesn't crash
XCTAssertNotNil(service)
}
func testStopTestMode() {
@@ -118,8 +194,8 @@ final class EnforceModeServiceTests: XCTestCase {
}
func testTestModeCycle() async {
await service.enableEnforceMode()
await service.startTestMode()
XCTAssertTrue(service.isTestMode)
service.stopTestMode()
XCTAssertFalse(service.isTestMode)

View File

@@ -1,217 +0,0 @@
//
// 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)
}
}