feat: preview video feed
This commit is contained in:
@@ -12,8 +12,10 @@ import Foundation
|
|||||||
class EnforceModeService: ObservableObject {
|
class EnforceModeService: ObservableObject {
|
||||||
static let shared = EnforceModeService()
|
static let shared = EnforceModeService()
|
||||||
|
|
||||||
@Published var isEnforceModeActive = false
|
@Published var isEnforceModeEnabled = false
|
||||||
|
@Published var isCameraActive = false
|
||||||
@Published var userCompliedWithBreak = false
|
@Published var userCompliedWithBreak = false
|
||||||
|
@Published var isTestMode = false
|
||||||
|
|
||||||
private var settingsManager: SettingsManager
|
private var settingsManager: SettingsManager
|
||||||
private var eyeTrackingService: EyeTrackingService
|
private var eyeTrackingService: EyeTrackingService
|
||||||
@@ -37,27 +39,36 @@ class EnforceModeService: ObservableObject {
|
|||||||
|
|
||||||
func enableEnforceMode() async {
|
func enableEnforceMode() async {
|
||||||
print("🔒 enableEnforceMode called")
|
print("🔒 enableEnforceMode called")
|
||||||
guard !isEnforceModeActive else {
|
guard !isEnforceModeEnabled else {
|
||||||
print("⚠️ Enforce mode already active")
|
print("⚠️ Enforce mode already enabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
let cameraService = CameraAccessService.shared
|
||||||
print("🔒 Starting eye tracking...")
|
if !cameraService.isCameraAuthorized {
|
||||||
try await eyeTrackingService.startEyeTracking()
|
do {
|
||||||
isEnforceModeActive = true
|
print("🔒 Requesting camera permission...")
|
||||||
print("✓ Enforce mode enabled")
|
try await cameraService.requestCameraAccess()
|
||||||
} catch {
|
} catch {
|
||||||
print("⚠️ Failed to enable enforce mode: \(error.localizedDescription)")
|
print("⚠️ Failed to get camera permission: \(error.localizedDescription)")
|
||||||
isEnforceModeActive = false
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard cameraService.isCameraAuthorized else {
|
||||||
|
print("❌ Camera permission denied")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnforceModeEnabled = true
|
||||||
|
print("✓ Enforce mode enabled (camera will activate before lookaway reminders)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func disableEnforceMode() {
|
func disableEnforceMode() {
|
||||||
guard isEnforceModeActive else { return }
|
guard isEnforceModeEnabled else { return }
|
||||||
|
|
||||||
eyeTrackingService.stopEyeTracking()
|
stopCamera()
|
||||||
isEnforceModeActive = false
|
isEnforceModeEnabled = false
|
||||||
userCompliedWithBreak = false
|
userCompliedWithBreak = false
|
||||||
print("✓ Enforce mode disabled")
|
print("✓ Enforce mode disabled")
|
||||||
}
|
}
|
||||||
@@ -67,7 +78,7 @@ class EnforceModeService: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func shouldEnforceBreak(for timerIdentifier: TimerIdentifier) -> Bool {
|
func shouldEnforceBreak(for timerIdentifier: TimerIdentifier) -> Bool {
|
||||||
guard isEnforceModeActive else { return false }
|
guard isEnforceModeEnabled else { return false }
|
||||||
guard settingsManager.settings.enforcementMode else { return false }
|
guard settingsManager.settings.enforcementMode else { return false }
|
||||||
|
|
||||||
switch timerIdentifier {
|
switch timerIdentifier {
|
||||||
@@ -78,8 +89,32 @@ class EnforceModeService: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startCameraForLookawayTimer(secondsRemaining: Int) async {
|
||||||
|
guard isEnforceModeEnabled else { return }
|
||||||
|
guard !isCameraActive else { return }
|
||||||
|
|
||||||
|
print("👁️ Starting camera for lookaway reminder (T-\(secondsRemaining)s)")
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await eyeTrackingService.startEyeTracking()
|
||||||
|
isCameraActive = true
|
||||||
|
print("✓ Camera active")
|
||||||
|
} catch {
|
||||||
|
print("⚠️ Failed to start camera: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopCamera() {
|
||||||
|
guard isCameraActive else { return }
|
||||||
|
|
||||||
|
print("👁️ Stopping camera")
|
||||||
|
eyeTrackingService.stopEyeTracking()
|
||||||
|
isCameraActive = false
|
||||||
|
userCompliedWithBreak = false
|
||||||
|
}
|
||||||
|
|
||||||
func checkUserCompliance() {
|
func checkUserCompliance() {
|
||||||
guard isEnforceModeActive else {
|
guard isCameraActive else {
|
||||||
userCompliedWithBreak = false
|
userCompliedWithBreak = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -89,22 +124,37 @@ class EnforceModeService: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handleGazeChange(lookingAtScreen: Bool) {
|
private func handleGazeChange(lookingAtScreen: Bool) {
|
||||||
guard isEnforceModeActive else { return }
|
guard isCameraActive else { return }
|
||||||
|
|
||||||
checkUserCompliance()
|
checkUserCompliance()
|
||||||
}
|
}
|
||||||
|
|
||||||
func startEnforcementForActiveReminder() {
|
func handleReminderDismissed() {
|
||||||
guard let engine = timerEngine else { return }
|
stopCamera()
|
||||||
guard let activeReminder = engine.activeReminder else { return }
|
}
|
||||||
|
|
||||||
switch activeReminder {
|
func startTestMode() async {
|
||||||
case .lookAwayTriggered:
|
guard isEnforceModeEnabled else { return }
|
||||||
if shouldEnforceBreak(for: .builtIn(.lookAway)) {
|
guard !isCameraActive else { return }
|
||||||
checkUserCompliance()
|
|
||||||
}
|
print("🧪 Starting test mode")
|
||||||
default:
|
isTestMode = true
|
||||||
break
|
|
||||||
|
do {
|
||||||
|
try await eyeTrackingService.startEyeTracking()
|
||||||
|
isCameraActive = true
|
||||||
|
print("✓ Test mode camera active")
|
||||||
|
} catch {
|
||||||
|
print("⚠️ Failed to start test mode camera: \(error.localizedDescription)")
|
||||||
|
isTestMode = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stopTestMode() {
|
||||||
|
guard isTestMode else { return }
|
||||||
|
|
||||||
|
print("🧪 Stopping test mode")
|
||||||
|
stopCamera()
|
||||||
|
isTestMode = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,13 @@ class EyeTrackingService: NSObject, ObservableObject {
|
|||||||
private var videoOutput: AVCaptureVideoDataOutput?
|
private var videoOutput: AVCaptureVideoDataOutput?
|
||||||
private let videoDataOutputQueue = DispatchQueue(label: "com.gaze.videoDataOutput", qos: .userInitiated)
|
private let videoDataOutputQueue = DispatchQueue(label: "com.gaze.videoDataOutput", qos: .userInitiated)
|
||||||
|
|
||||||
|
var previewLayer: AVCaptureVideoPreviewLayer? {
|
||||||
|
guard let session = captureSession else { return nil }
|
||||||
|
let layer = AVCaptureVideoPreviewLayer(session: session)
|
||||||
|
layer.videoGravity = .resizeAspectFill
|
||||||
|
return layer
|
||||||
|
}
|
||||||
|
|
||||||
private override init() {
|
private override init() {
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,10 +80,7 @@ class TimerEngine: ObservableObject {
|
|||||||
|
|
||||||
/// Check if enforce mode is active and should affect timer behavior
|
/// Check if enforce mode is active and should affect timer behavior
|
||||||
func checkEnforceMode() {
|
func checkEnforceMode() {
|
||||||
guard let enforceService = enforceModeService else { return }
|
// Deprecated - camera is now activated in handleTick before timer triggers
|
||||||
guard enforceService.isEnforceModeActive else { return }
|
|
||||||
|
|
||||||
enforceService.startEnforcementForActiveReminder()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateConfigurations() {
|
private func updateConfigurations() {
|
||||||
@@ -210,10 +207,11 @@ class TimerEngine: ObservableObject {
|
|||||||
guard let reminder = activeReminder else { return }
|
guard let reminder = activeReminder else { return }
|
||||||
activeReminder = nil
|
activeReminder = nil
|
||||||
|
|
||||||
// Skip to next interval and resume the timer that was paused
|
|
||||||
let identifier = reminder.identifier
|
let identifier = reminder.identifier
|
||||||
skipNext(identifier: identifier)
|
skipNext(identifier: identifier)
|
||||||
resumeTimer(identifier: identifier)
|
resumeTimer(identifier: identifier)
|
||||||
|
|
||||||
|
enforceModeService?.handleReminderDismissed()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleTick() {
|
private func handleTick() {
|
||||||
@@ -228,13 +226,23 @@ class TimerEngine: ObservableObject {
|
|||||||
|
|
||||||
timerStates[identifier]?.remainingSeconds -= 1
|
timerStates[identifier]?.remainingSeconds -= 1
|
||||||
|
|
||||||
if let updatedState = timerStates[identifier], updatedState.remainingSeconds <= 0 {
|
if let updatedState = timerStates[identifier] {
|
||||||
triggerReminder(for: identifier)
|
if updatedState.remainingSeconds <= 3 && !updatedState.isPaused {
|
||||||
break
|
if case .builtIn(.lookAway) = identifier {
|
||||||
|
if enforceModeService?.shouldEnforceBreak(for: identifier) == true {
|
||||||
|
Task { @MainActor in
|
||||||
|
await enforceModeService?.startCameraForLookawayTimer(secondsRemaining: updatedState.remainingSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatedState.remainingSeconds <= 0 {
|
||||||
|
triggerReminder(for: identifier)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkEnforceMode()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func triggerReminder(for identifier: TimerIdentifier) {
|
func triggerReminder(for identifier: TimerIdentifier) {
|
||||||
|
|||||||
48
Gaze/Views/Components/CameraPreviewView.swift
Normal file
48
Gaze/Views/Components/CameraPreviewView.swift
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
//
|
||||||
|
// CameraPreviewView.swift
|
||||||
|
// Gaze
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/14/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
struct CameraPreviewView: NSViewRepresentable {
|
||||||
|
let previewLayer: AVCaptureVideoPreviewLayer
|
||||||
|
let borderColor: NSColor
|
||||||
|
|
||||||
|
func makeNSView(context: Context) -> NSView {
|
||||||
|
let view = PreviewContainerView()
|
||||||
|
view.wantsLayer = true
|
||||||
|
|
||||||
|
previewLayer.frame = view.bounds
|
||||||
|
view.layer?.addSublayer(previewLayer)
|
||||||
|
|
||||||
|
updateBorder(view: view, color: borderColor)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ nsView: NSView, context: Context) {
|
||||||
|
previewLayer.frame = nsView.bounds
|
||||||
|
updateBorder(view: nsView, color: borderColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateBorder(view: NSView, color: NSColor) {
|
||||||
|
view.layer?.borderColor = color.cgColor
|
||||||
|
view.layer?.borderWidth = 4
|
||||||
|
view.layer?.cornerRadius = 12
|
||||||
|
view.layer?.masksToBounds = true
|
||||||
|
}
|
||||||
|
|
||||||
|
class PreviewContainerView: NSView {
|
||||||
|
override func layout() {
|
||||||
|
super.layout()
|
||||||
|
// Update sublayer frames when view is resized
|
||||||
|
if let previewLayer = layer?.sublayers?.first as? AVCaptureVideoPreviewLayer {
|
||||||
|
previewLayer.frame = bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ struct EnforceModeSetupView: View {
|
|||||||
@ObservedObject var enforceModeService = EnforceModeService.shared
|
@ObservedObject var enforceModeService = EnforceModeService.shared
|
||||||
|
|
||||||
@State private var isProcessingToggle = false
|
@State private var isProcessingToggle = false
|
||||||
|
@State private var isTestModeActive = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
@@ -40,7 +41,7 @@ struct EnforceModeSetupView: View {
|
|||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Enable Enforce Mode")
|
Text("Enable Enforce Mode")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Text("Uses camera to detect when you look away from screen")
|
Text("Camera activates 3 seconds before lookaway reminders")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
@@ -70,11 +71,21 @@ struct EnforceModeSetupView: View {
|
|||||||
|
|
||||||
cameraStatusView
|
cameraStatusView
|
||||||
|
|
||||||
if enforceModeService.isEnforceModeActive {
|
if enforceModeService.isEnforceModeEnabled {
|
||||||
eyeTrackingStatusView
|
testModeButton
|
||||||
}
|
}
|
||||||
|
|
||||||
privacyInfoView
|
if isTestModeActive && enforceModeService.isCameraActive {
|
||||||
|
testModePreviewView
|
||||||
|
} else {
|
||||||
|
if enforceModeService.isCameraActive && !isTestModeActive {
|
||||||
|
eyeTrackingStatusView
|
||||||
|
} else if enforceModeService.isEnforceModeEnabled {
|
||||||
|
cameraPendingView
|
||||||
|
}
|
||||||
|
|
||||||
|
privacyInfoView
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +96,71 @@ struct EnforceModeSetupView: View {
|
|||||||
.background(.clear)
|
.background(.clear)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var testModeButton: some View {
|
||||||
|
Button(action: {
|
||||||
|
Task { @MainActor in
|
||||||
|
if isTestModeActive {
|
||||||
|
enforceModeService.stopTestMode()
|
||||||
|
isTestModeActive = false
|
||||||
|
} else {
|
||||||
|
await enforceModeService.startTestMode()
|
||||||
|
isTestModeActive = enforceModeService.isCameraActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: isTestModeActive ? "stop.circle.fill" : "play.circle.fill")
|
||||||
|
.font(.title3)
|
||||||
|
Text(isTestModeActive ? "Stop Test" : "Test Tracking")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.controlSize(.large)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var testModePreviewView: some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
if let previewLayer = eyeTrackingService.previewLayer {
|
||||||
|
let lookingAway = !eyeTrackingService.userLookingAtScreen
|
||||||
|
let borderColor: NSColor = lookingAway ? .systemGreen : .systemRed
|
||||||
|
|
||||||
|
CameraPreviewView(previewLayer: previewLayer, borderColor: borderColor)
|
||||||
|
.frame(height: 300)
|
||||||
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("Live Tracking Status")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
statusIndicator(
|
||||||
|
title: "Face Detected",
|
||||||
|
isActive: eyeTrackingService.faceDetected,
|
||||||
|
icon: "person.fill"
|
||||||
|
)
|
||||||
|
|
||||||
|
statusIndicator(
|
||||||
|
title: "Looking Away",
|
||||||
|
isActive: !eyeTrackingService.userLookingAtScreen,
|
||||||
|
icon: "arrow.turn.up.right"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(lookingAway ? "✓ Break compliance detected" : "⚠️ Please look away from screen")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(lookingAway ? .green : .orange)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var cameraStatusView: some View {
|
private var cameraStatusView: some View {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
@@ -140,15 +216,9 @@ struct EnforceModeSetupView: View {
|
|||||||
)
|
)
|
||||||
|
|
||||||
statusIndicator(
|
statusIndicator(
|
||||||
title: "Looking at Screen",
|
title: "Looking Away",
|
||||||
isActive: eyeTrackingService.userLookingAtScreen,
|
isActive: !eyeTrackingService.userLookingAtScreen,
|
||||||
icon: "eye.fill"
|
icon: "arrow.turn.up.right"
|
||||||
)
|
|
||||||
|
|
||||||
statusIndicator(
|
|
||||||
title: "Eyes Closed",
|
|
||||||
isActive: eyeTrackingService.isEyesClosed,
|
|
||||||
icon: "eye.slash.fill"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,6 +226,26 @@ struct EnforceModeSetupView: View {
|
|||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var cameraPendingView: some View {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "timer")
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Camera Ready")
|
||||||
|
.font(.headline)
|
||||||
|
Text("Will activate 3 seconds before lookaway reminder")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
|
}
|
||||||
|
|
||||||
private func statusIndicator(title: String, isActive: Bool, icon: String) -> some View {
|
private func statusIndicator(title: String, isActive: Bool, icon: String) -> some View {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
@@ -183,7 +273,8 @@ struct EnforceModeSetupView: View {
|
|||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
privacyBullet("All processing happens on-device")
|
privacyBullet("All processing happens on-device")
|
||||||
privacyBullet("No images are stored or transmitted")
|
privacyBullet("No images are stored or transmitted")
|
||||||
privacyBullet("Camera only active during lookaway reminders")
|
privacyBullet("Camera only active during lookaway reminders (3 second window)")
|
||||||
|
privacyBullet("Eyes closed does not affect countdown")
|
||||||
privacyBullet("You can disable at any time")
|
privacyBullet("You can disable at any time")
|
||||||
}
|
}
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
@@ -212,9 +303,9 @@ struct EnforceModeSetupView: View {
|
|||||||
if enabled {
|
if enabled {
|
||||||
print("🎛️ Enabling enforce mode...")
|
print("🎛️ Enabling enforce mode...")
|
||||||
await enforceModeService.enableEnforceMode()
|
await enforceModeService.enableEnforceMode()
|
||||||
print("🎛️ Enforce mode enabled, isActive: \(enforceModeService.isEnforceModeActive)")
|
print("🎛️ Enforce mode enabled: \(enforceModeService.isEnforceModeEnabled)")
|
||||||
|
|
||||||
if !enforceModeService.isEnforceModeActive {
|
if !enforceModeService.isEnforceModeEnabled {
|
||||||
print("⚠️ Failed to activate, reverting toggle")
|
print("⚠️ Failed to activate, reverting toggle")
|
||||||
settingsManager.settings.enforcementMode = false
|
settingsManager.settings.enforcementMode = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,14 +25,16 @@ final class EnforceModeServiceTests: XCTestCase {
|
|||||||
|
|
||||||
func testEnforceModeServiceInitialization() {
|
func testEnforceModeServiceInitialization() {
|
||||||
XCTAssertNotNil(enforceModeService)
|
XCTAssertNotNil(enforceModeService)
|
||||||
XCTAssertFalse(enforceModeService.isEnforceModeActive)
|
XCTAssertFalse(enforceModeService.isEnforceModeEnabled)
|
||||||
|
XCTAssertFalse(enforceModeService.isCameraActive)
|
||||||
XCTAssertFalse(enforceModeService.userCompliedWithBreak)
|
XCTAssertFalse(enforceModeService.userCompliedWithBreak)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDisableEnforceModeResetsState() {
|
func testDisableEnforceModeResetsState() {
|
||||||
enforceModeService.disableEnforceMode()
|
enforceModeService.disableEnforceMode()
|
||||||
|
|
||||||
XCTAssertFalse(enforceModeService.isEnforceModeActive)
|
XCTAssertFalse(enforceModeService.isEnforceModeEnabled)
|
||||||
|
XCTAssertFalse(enforceModeService.isCameraActive)
|
||||||
XCTAssertFalse(enforceModeService.userCompliedWithBreak)
|
XCTAssertFalse(enforceModeService.userCompliedWithBreak)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user