Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions HomeAssistant.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1246,11 +1246,13 @@
54B2C38995814098B677135A /* WidgetCommonlyUsedEntities.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEEEA3344F064DB183F46C47 /* WidgetCommonlyUsedEntities.swift */; };
58213D5B1792311CD4CA261D /* Pods_iOS_Extensions_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6DA82FEEE2DDC3B2CC20DA3 /* Pods_iOS_Extensions_NotificationService.framework */; };
5B715903CB3450FE351399BC /* Pods-iOS-Extensions-Share-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 207E35C8F1554A9AD616FFA2 /* Pods-iOS-Extensions-Share-metadata.plist */; };
5C03030E951BE417C7CB03E7 /* KioskCameraDetectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F06E4C4BCA8BCEFAC2BD8CD /* KioskCameraDetectionManager.swift */; };
5D4737422F241342009A70EA /* FolderDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D4737412F241342009A70EA /* FolderDetailView.swift */; };
5F2ECF3C2CA505A59A1CFFAF /* Pods_iOS_SharedTesting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65B4DC669522BB7A70C5EED0 /* Pods_iOS_SharedTesting.framework */; };
61495A70232316478717CF27 /* Pods_iOS_Shared_iOS_Tests_Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FF3A67FB1C2B548C6C7730C /* Pods_iOS_Shared_iOS_Tests_Shared.framework */; };
618FCE5CA6B34267BB2056F5 /* MockLiveActivityRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E95733B72864AB3B9607B57 /* MockLiveActivityRegistry.swift */; };
651755E378F6F79AB401F05C /* AssistPipelineAddList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07701F2786F6D45E945CC1AA /* AssistPipelineAddList.swift */; };
64CABB447201BA52BF13159B /* KioskCameraDetection.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EAFCF875FE1A016DCEA9CA /* KioskCameraDetection.test.swift */; };
65286F3B745551AD4090EE6B /* Pods-iOS-SharedTesting-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4053903E4C54A6803204286E /* Pods-iOS-SharedTesting-metadata.plist */; };
6596FA74E1A501276EA62D86 /* Pods_watchOS_Shared_watchOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD370D44DFFB906B05C3EB3A /* Pods_watchOS_Shared_watchOS.framework */; };
692BCBBA4EEEABCC76DBBECA /* Database/GRDB+Initialization.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C50FA39BF16AD0BD782D0D7 /* Database/GRDB+Initialization.test.swift */; };
Expand Down Expand Up @@ -1539,6 +1541,7 @@
C1AE883A374C598B5BCCAE23 /* CustomWidgetIntentHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A7FF77B68D19A4AD68F8FE3 /* CustomWidgetIntentHelper.swift */; };
C35621B95F7E4548BC8F6D75 /* FolderEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BECEB2525564358A124F818 /* FolderEditView.swift */; };
C3EB3740FA097F36D51F525E /* BarometerSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1396822195C7FF562AB891F2 /* BarometerSensor.swift */; };
C3D543FC10EC39A66961A92D /* KioskPresenceDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64EAB9B84E4838281F3C36B /* KioskPresenceDetector.swift */; };
C574CE3276BCE901743FF8C9 /* KioskSettings.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFD4B475DDA9447E45A9BAD3 /* KioskSettings.test.swift */; };
C8860D27D848451A887BC441 /* WatchFolderRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DA2D62699FC44A99AB37480 /* WatchFolderRow.swift */; };
CA0CA15000000000000000A2 /* LocationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0CA15000000000000000A1 /* LocationSettingsView.swift */; };
Expand Down Expand Up @@ -1593,6 +1596,8 @@
DB54626ADCE0C32094C8C0B9 /* LoadingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BFD0D30836C69840AB63A8A /* LoadingButton.swift */; };
DEFBE1A5E9A005B0A5392D27 /* KioskLocalization.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F4593A60DBF019E6C91AAA7 /* KioskLocalization.test.swift */; };
E3A02409794174F002C8BB4F /* IconSearchPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC23DE131CA8813C2DBD657 /* IconSearchPicker.swift */; };
DEFBE1A5E9A005B0A5392D27 /* KioskLocalization.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F4593A60DBF019E6C91AAA7 /* KioskLocalization.test.swift */; };
E72C0153D13C2041C76CE742 /* KioskCameraMotionDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 596D41DEAF91D00E446816EC /* KioskCameraMotionDetector.swift */; };
E92E09E3A93650D56E3C5093 /* KioskScreensaverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CDCFDB29D283A7902A3ABE /* KioskScreensaverViewController.swift */; };
F0D1DD41A8F55F6D767EBF37 /* TemplatePreviewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 332C060487B468055C487070 /* TemplatePreviewSection.swift */; };
FC8E9421FDB864726918B612 /* Pods-watchOS-WatchExtension-Watch-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9249824D575933DFA1530BB2 /* Pods-watchOS-WatchExtension-Watch-metadata.plist */; };
Expand Down Expand Up @@ -3098,6 +3103,11 @@
5D4737412F241342009A70EA /* FolderDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderDetailView.swift; sourceTree = "<group>"; };
5E95733B72864AB3B9607B57 /* MockLiveActivityRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockLiveActivityRegistry.swift; sourceTree = "<group>"; };
5FF9C3A30E10A8E214623EBB /* ComplicationListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ComplicationListView.swift; sourceTree = "<group>"; };
596D41DEAF91D00E446816EC /* KioskCameraMotionDetector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = KioskCameraMotionDetector.swift; sourceTree = "<group>"; };
5C50FA39BF16AD0BD782D0D7 /* GRDB+Initialization.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Database/GRDB+Initialization.test.swift"; sourceTree = "<group>"; };
5D4737412F241342009A70EA /* FolderDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderDetailView.swift; sourceTree = "<group>"; };
5E95733B72864AB3B9607B57 /* MockLiveActivityRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockLiveActivityRegistry.swift; sourceTree = "<group>"; };
5F06E4C4BCA8BCEFAC2BD8CD /* KioskCameraDetectionManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = KioskCameraDetectionManager.swift; sourceTree = "<group>"; };
608CFDA223EBCDF01B946093 /* Pods_iOS_Extensions_PushProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOS_Extensions_PushProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
62CDCFDB29D283A7902A3ABE /* KioskScreensaverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KioskScreensaverViewController.swift; sourceTree = "<group>"; };
6563AFB7BDAF57478CA18D9B /* Pods-iOS-Extensions-PushProvider.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS-Extensions-PushProvider.debug.xcconfig"; path = "Pods/Target Support Files/Pods-iOS-Extensions-PushProvider/Pods-iOS-Extensions-PushProvider.debug.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3508,6 +3518,7 @@
D0EEF321214DE56B00D1D360 /* LocationTrigger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationTrigger.swift; sourceTree = "<group>"; };
D0FF79CB20D778B50034574D /* ClientEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientEvent.swift; sourceTree = "<group>"; };
D0FF79CD20D85C3A0034574D /* ClientEventStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientEventStore.swift; sourceTree = "<group>"; };
D4EAFCF875FE1A016DCEA9CA /* KioskCameraDetection.test.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = KioskCameraDetection.test.swift; sourceTree = "<group>"; };
D72C761F65606EF882E2A7B1 /* Pods-iOS-Extensions-Today-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-iOS-Extensions-Today-metadata.plist"; path = "Pods/Pods-iOS-Extensions-Today-metadata.plist"; sourceTree = "<group>"; };
D790D5FA5DBB4B5B9DBB2334 /* NotificationsCommandManagerLiveActivityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsCommandManagerLiveActivityTests.swift; sourceTree = "<group>"; };
DEEEA3344F064DB183F46C47 /* WidgetCommonlyUsedEntities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetCommonlyUsedEntities.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3720,6 +3731,7 @@
06D62F8A8D381DAFB70C6B31 /* Kiosk */ = {
isa = PBXGroup;
children = (
D4EAFCF875FE1A016DCEA9CA /* KioskCameraDetection.test.swift */,
4F4593A60DBF019E6C91AAA7 /* KioskLocalization.test.swift */,
EFD4B475DDA9447E45A9BAD3 /* KioskSettings.test.swift */,
373AE72CB925F044BAE18B62 /* KioskLifecycleBrightness.test.swift */,
Expand Down Expand Up @@ -6544,12 +6556,13 @@
5F7F99C4E4A98B841C0969B6 /* Kiosk */ = {
isa = PBXGroup;
children = (
825E1E44BA9ABF1BF53733D3 /* KioskConstants.swift */,
C00AE2FDC80CA2FFDFCA2B2B /* KioskModeManager.swift */,
402432B9CC897C6278B08A79 /* KioskSettings.swift */,
98096A4025810F739C3A581B /* Camera */,
82F2464E2BC5B4C6E667087B /* Overlay */,
4C1A049B16335C08AECDAAC2 /* Screensaver */,
D6500FB6C2035F49B7421ED9 /* Settings */,
825E1E44BA9ABF1BF53733D3 /* KioskConstants.swift */,
C00AE2FDC80CA2FFDFCA2B2B /* KioskModeManager.swift */,
402432B9CC897C6278B08A79 /* KioskSettings.swift */,
);
path = Kiosk;
sourceTree = "<group>";
Expand Down Expand Up @@ -6581,6 +6594,16 @@
path = CommonlyUsedEntities;
sourceTree = "<group>";
};
98096A4025810F739C3A581B /* Camera */ = {
isa = PBXGroup;
children = (
5F06E4C4BCA8BCEFAC2BD8CD /* KioskCameraDetectionManager.swift */,
596D41DEAF91D00E446816EC /* KioskCameraMotionDetector.swift */,
);
name = Camera;
path = Camera;
sourceTree = "<group>";
};
9C4E5E20229D97FA0044C8EC /* Configuration */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -9747,6 +9770,8 @@
D6F87818FFB55C006563EBBB /* RealmResultsObserver.swift in Sources */,
8411EA28D9F5FEF26D01A39E /* CameraZoomGestureOverlay.swift in Sources */,
D9BF1EFF40733A4A1D03B9C8 /* CustomWidgetIntentHelper.swift in Sources */,
5C03030E951BE417C7CB03E7 /* KioskCameraDetectionManager.swift in Sources */,
E72C0153D13C2041C76CE742 /* KioskCameraMotionDetector.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -9832,6 +9857,7 @@
C574CE3276BCE901743FF8C9 /* KioskSettings.test.swift in Sources */,
DEFBE1A5E9A005B0A5392D27 /* KioskLocalization.test.swift in Sources */,
A1619F1ED93FB8B0E7E53C38 /* KioskLifecycleBrightness.test.swift in Sources */,
64CABB447201BA52BF13159B /* KioskCameraDetection.test.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
118 changes: 118 additions & 0 deletions Sources/App/Kiosk/Camera/KioskCameraDetectionManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import AVFoundation
import Combine
import Foundation
import Shared
import UIKit

// MARK: - Kiosk Camera Detection Manager

/// Coordinates camera-based motion detection for kiosk mode
@MainActor
public final class KioskCameraDetectionManager: ObservableObject {
// MARK: - Singleton

public static let shared = KioskCameraDetectionManager()

// MARK: - Published State

/// Whether any camera detection is currently active
@Published public private(set) var isActive: Bool = false

/// Current motion detected state
@Published public private(set) var motionDetected: Bool = false

/// Camera authorization status
@Published public private(set) var authorizationStatus: AVAuthorizationStatus = .notDetermined

// MARK: - Callbacks

/// Called when motion is detected (for wake trigger)
public var onMotionDetected: (() -> Void)?

// MARK: - Private

private var settings: KioskSettings { KioskModeManager.shared.settings }
private let motionDetector = KioskCameraMotionDetector()
private var cancellables = Set<AnyCancellable>()

// MARK: - Initialization

private init() {
setupBindings()
checkAuthorizationStatus()
}

deinit {
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
}

// MARK: - Public Methods

/// Start camera detection based on current settings.
/// `isActive` reflects the underlying detector state (bound in setupBindings),
/// so it stays false if the detector bails out (e.g. camera permission denied).
public func start() {
Current.Log.info("Starting camera detection manager")

if settings.cameraMotionEnabled {
motionDetector.start()
}
}
Comment thread
nstefanelli marked this conversation as resolved.

/// Stop all camera detection
public func stop() {
Current.Log.info("Stopping camera detection manager")
motionDetector.stop()
}

/// Restart detection (e.g., after settings change)
public func restart() {
stop()
start()
}

/// Request camera authorization
public func requestAuthorization() async -> Bool {
let granted = await motionDetector.requestAuthorization()
checkAuthorizationStatus()
return granted
}

// MARK: - Private Methods

private func checkAuthorizationStatus() {
authorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
}

private func setupBindings() {
motionDetector.$isActive
.receive(on: DispatchQueue.main)
.sink { [weak self] active in
self?.isActive = active
}
.store(in: &cancellables)

motionDetector.$motionDetected
.receive(on: DispatchQueue.main)
.sink { [weak self] detected in
self?.motionDetected = detected
if detected {
self?.handleMotionDetected()
}
}
.store(in: &cancellables)

motionDetector.$authorizationStatus
.receive(on: DispatchQueue.main)
.sink { [weak self] status in
self?.authorizationStatus = status
}
.store(in: &cancellables)
}

private func handleMotionDetected() {
Current.Log.info("Camera motion detected")
onMotionDetected?()
}
}
Loading
Loading