Skip to content

Add kiosk mode core infrastructure (PR 1/5)#4422

Open
nstefanelli wants to merge 41 commits intohome-assistant:mainfrom
nstefanelli:kiosk-pr1-core
Open

Add kiosk mode core infrastructure (PR 1/5)#4422
nstefanelli wants to merge 41 commits intohome-assistant:mainfrom
nstefanelli:kiosk-pr1-core

Conversation

@nstefanelli
Copy link

Summary

Reopening kiosk mode PR (previously #4218) after addressing all review feedback from @bgoncal. Apologies for the delay — some family matters pulled me away, but I'm excited to get this implemented and am fully committed to seeing it through.

This PR adds the foundational infrastructure for kiosk mode — a feature designed for wall-mounted iPad displays running Home Assistant dashboards.

What's included:

  • KioskModeManager — Central coordinator singleton with KioskModeObserver protocol (no NotificationCenter)
  • KioskSettings — Comprehensive settings model with GRDB persistence (camelCase columns, Codable)
  • Screensaver system — Clock screensaver with 4 styles, pixel shift for OLED burn-in prevention
  • Secret exit gesture — Configurable corner tap to access settings when locked down
  • Status bar hiding — StatusBarForwardingNavigationController for proper UIKit integration
  • Settings UI — SwiftUI settings accessible via Settings menu and secret gesture
  • 28 unit tests — Covering settings serialization, time logic, orientations, enums

Changes since #4218 (all review feedback addressed):

  • ✅ Fixed settings modal dismissal on iOS 15 (UINavigationController + explicit onDismiss closure)
  • ✅ Fixed status bar hiding (StatusBarForwardingNavigationController + statusBarView integration)
  • ✅ Removed non-functional settings (navigationLockdown, TouchFeedbackManager, IconMapper)
  • ✅ Replaced Date() with Current.date() for testability
  • ✅ GRDB persistence follows CarPlayConfig/WatchConfig pattern with DatabaseTableProtocol
  • ✅ All strings localized via L10n (SwiftGen)
  • ✅ SFSafeSymbols used throughout (no string-based systemName)
  • ✅ KioskModeObserver protocol (no NotificationCenter)
  • ✅ Kiosk files prefixed with Kiosk
  • ✅ UIKit screensaver container (required for status bar/home indicator control)
  • ✅ macCatalyst filtered out
  • ✅ Merged with latest upstream/main (adapted to Frontend/ directory restructure)
  • ✅ Idle timer pauses when settings view is open
  • ✅ TODO comments (no PR-specific comments)

Testing Instructions

  1. Settings → Companion App → Kiosk Mode → Enable "Enable Kiosk Mode"
  2. Set screensaver timeout to 30 seconds for quick testing
  3. Wait for screensaver → tap to wake
  4. Use secret gesture (default: 3 taps in top-left corner) to access settings

Previous review context

All feedback from @bgoncal in #4218 has been addressed. See that PR for full discussion history.

Test plan

  • Enable/disable kiosk mode from Settings
  • Screensaver activates after timeout
  • Tap to wake from screensaver
  • Secret gesture opens settings
  • Status bar hides when kiosk mode active (full-screen iPad only)
  • Device authentication toggle works
  • Settings persist across app restart
  • Clock screensaver styles (large, minimal, digital, analog)
  • Pixel shift moves clock position periodically
  • Kiosk entry hidden on macCatalyst

🤖 Generated with Claude Code

nstefanelli and others added 27 commits January 13, 2026 12:46
Implement foundational kiosk mode functionality for wall-mounted displays:

Core Components:
- KioskModeManager: Central singleton coordinating screen state, brightness,
  screensaver timing, and settings persistence
- KioskSettings: Comprehensive settings model (~100 configurable options)
  with full Codable support for sync with Home Assistant
- KioskConstants: Centralized constants for timing, UI sizing, and defaults

Screensaver System:
- ScreensaverViewController: UIKit host managing screensaver lifecycle
- ClockScreensaverView: SwiftUI clock with large/minimal/digital/analog styles
- Pixel shift support for OLED burn-in prevention
- Brightness scheduling (day/night modes)

Settings & Security:
- KioskSettingsView: SwiftUI settings interface with grouped sections
- SecretExitGestureView: Configurable corner tap gesture to access settings
- Biometric authentication support for exiting kiosk mode

Utilities:
- IconMapper: MDI to SF Symbols translation
- TouchFeedbackManager: Haptic/audio feedback coordination
- AnimationUtilities: Shared animation configurations

Integration:
- WebViewController+Kiosk extension for WebView integration
- Callback-based architecture for loose coupling

This is PR 1 of 5 implementing kiosk mode. Future PRs will add:
- PR 2: HA entity integration and sensors
- PR 3: Remote commands via notifications
- PR 4: Camera motion/presence detection
- PR 5: Photo screensaver and advanced features

Discussion: https://github.com/orgs/home-assistant/discussions/2403
Address PR review feedback: Replace all Image(systemName:) and
Label(..., systemImage:) with type-safe SFSafeSymbols equivalents
using systemSymbol: parameter.
Address PR review feedback requesting test coverage for kiosk mode.

Tests added for:
- KioskSettings Codable encode/decode roundtrip
- DashboardConfig, EntityTrigger, ClockEntityConfig serialization
- QuickAction and TriggerAction enum encoding
- TimeOfDay comparison logic (isBefore, edge cases)
- DeviceOrientation matching (including landscape variants)
- Enum display names (ScreensaverMode, ClockStyle, ScreenCorner)
- CameraPopupSize parameters
- IconMapper MDI to SF Symbol translation

Total: 28 new test cases covering core kiosk functionality.
Changes:
- Rename files with Kiosk prefix (SecretExitGestureView, ScreensaverViewController, ClockScreensaverView)
- Replace NotificationCenter with KioskModeObserver protocol for settings/mode/pixel-shift notifications
- Change PR contextualization comments to TODO comments
- Localize all hardcoded strings to L10n in KioskSettingsView
- Use TouchFeedbackManager instead of inline UIImpactFeedbackGenerator
- Add setupKioskMode() call to WebViewController for testing
- Default biometric/passcode auth to disabled
- Skip auth on exit if both auth methods are disabled
- Require authentication to access settings while in kiosk mode (if auth is enabled)
- Dismiss settings if user cancels authentication
- Wire up kioskPrefersStatusBarHidden/kioskPrefersHomeIndicatorAutoHidden
  in WebViewController to actually hide status bar when kiosk mode is active
- Add 30-second screensaver timeout option to picker
- Add corresponding L10n localization string for 30sec timeout
- Fix iPad split view: add .navigationViewStyle(.stack) to settings modal
- Fix auth bypass: capture authRequired at init, use persisted settings for exit auth
- Fix toggle consistency: always use toggle for enable/disable kiosk mode
- Fix auth dismiss: delay auth prompt to let modal fully present
- Remove unnecessary Screen/Screensaver status labels from settings
- Add pauseIdleTimer/resumeIdleTimer to prevent screensaver while settings open
- Settings view pauses idle timer on appear, resumes on disappear
- Remove Lock Navigation setting (not implemented in this PR)
- Add Done button to toolbar for closing settings when opened via secret gesture
- Fix settings modal dismissal bug by removing aggressive dismiss() calls
  on auth cancellation; add interactive overlay with Cancel/Try Again buttons
- Remove TouchFeedbackManager entirely (iPads don't have haptic feedback)
- Migrate KioskSettings persistence to GRDB using FetchableRecord/PersistableRecord
  pattern matching CarPlayConfig and WatchConfig
- Consolidate biometric/passcode auth toggles into single "Device Authentication"
  toggle since iOS always prefers biometric when enrolled
- Add KioskSettingsRecord wrapper for JSON blob storage in SQLite
- Update localization strings for new auth UI
- Revert unrelated Podfile.lock changes
- Restore public modifiers on WebViewController methods
- Change settingsJSON column to camelCase convention
- Remove objc associated objects, move properties to WebViewController
- Remove comments from Localizable.strings
- Remove unused IconMapper.swift and AnimationUtilities.swift
- Use DesignSystem values for spacing/cornerRadius in KioskConstants
- Use Current.date() in KioskClockScreensaverView for testability
- Use NavigationStack on iOS 16+ to fix modal dismiss issue
…ructure

Upstream restructured WebView/ → Frontend/ and split WebViewController into
multiple files. Adapted kiosk mode integration:
- Moved WebViewController+Kiosk.swift to Frontend/Extensions/
- Added kiosk stored properties and Combine import to new WebViewController.swift
- Added StatusBarForwardingNavigationController to new WebViewWindowController.swift
- Fixed status bar hiding (UINavigationController forwarding + statusBarView)
- Fixed settings modal dismissal (UINavigationController + onDismiss closure)
- Replaced Date() with Current.date() for testability
- Removed navigationLockdown placeholder setting
- Fixed test references to removed properties (allowBiometricExit → requireDeviceAuthentication)
- Removed IconMapper tests (class was removed in earlier review feedback)
SwiftGen needs to be re-run after adding kiosk L10n keys to Localizable.strings.
…bility

- Rebuilt project.pbxproj with proper kiosk file references
- Updated KioskSettingsTable to conform to new DatabaseTableProtocol
  (added tableName, definedColumns properties, migrateColumns call)
- Added CaseIterable to DatabaseTables.KioskSettings enum
Copilot AI review requested due to automatic review settings March 9, 2026 13:02
@bgoncal

This comment was marked as resolved.

@bgoncal
Copy link
Member

bgoncal commented Mar 10, 2026

Clock is only relevant for screensaver, so it should be part of the screensaver section and be hidden if screensaver is off
Simulator Screenshot - iPad Air 11-inch (M3) - 2026-03-10 at 12 12 00

@bgoncal
Copy link
Member

bgoncal commented Mar 10, 2026

Day/night schedule can be moved to a separate PR

Simulator Screenshot - iPad Air 11-inch (M3) - 2026-03-10 at 12 13 16

…RDB simplification

Architecture:
- Create KioskModeHandler with weak WebViewControllerProtocol and DI for KioskModeManager
- WebViewController+Kiosk.swift is now a thin delegation layer
- Present screensaver as fullscreen VC (.overFullScreen) instead of child view
- Remove delay workaround in hideScreensaver — dismiss handles animation
- Dismiss screensaver before presenting settings modal

GRDB:
- Simplify KioskSettingsRecord — remove manual JSON encode/decode
- Use .jsonText column type so GRDB handles Codable automatically
- Follows same pattern as CarPlayConfig

Strip unimplemented features (PR1 only):
- KioskSettings: ~1063 → ~230 lines, keep only 25 implemented settings
- Remove 10+ unused types (DashboardConfig, EntityTrigger, PhotoSource, etc.)
- ScreensaverMode: 7 → 3 cases (blank, dim, clock)
- KioskConstants: remove Motion, Audio, Battery, Panel, Shadow enums
- KioskModeManager: remove unused callbacks and wake logic
- Remove TODO comments and placeholder settings from UI

Small fixes:
- Extract KioskDateFormatters enum (reusable cached formatters)
- Use .background(Color.black.ignoresSafeArea()) instead of ZStack
- Use DesignSystem.Spaces.two for spacing
- All displayNames use L10n localization keys
…ns when unused

- Remove TimeOfDay struct, brightnessScheduleEnabled, dayBrightness, nightBrightness,
  dayStartTime, nightStartTime settings
- Remove isNightTime(), scheduleBrightnessUpdate(), brightnessTimer from KioskModeManager
- Remove TimeOfDayPicker and schedule UI from KioskSettingsView
- Remove scheduleCheckInterval from KioskConstants
- Remove related L10n keys and tests
- Simplify brightness to manual-only (applyBrightness)
- Clock options section now only shows when screensaver is enabled and mode is clock
@nstefanelli
Copy link
Author

Let's remove "device authentication" from this PR and add to a following one, it's currently not working and I dont think it's necessary for the foundation of kiosk mode, then in a separate PR we can dedicate testing exclusively to it

What the video demonstrates:

  • Enabling kiosk mode from Settings
  • Clock screensaver activation after idle timeout
  • Wake-on-touch dismissing screensaver
  • Secret corner gesture accessing settings while in kiosk mode
  • Device authentication (passcode + Face ID) gating settings access
  • Disabling kiosk mode with auth required
  • No lock-out states — secret gesture is always available as escape hatch

All three screensaver modes (clock, dim, blank) and all four clock styles are functional — video focuses on the core enable/auth/exit flow.

I'm happy to pull auth - but it has been working in my testing - can you help to identify where you're seeing an issue?

Screensaver.Test.and.Exit.mov
Enable.and.Auth.Test.mov

@nstefanelli nstefanelli marked this pull request as ready for review March 11, 2026 01:26
@home-assistant home-assistant bot requested a review from bgoncal March 11, 2026 01:26
Comment on lines +171 to +174
public enum AppState: String, Codable {
case active = "active"
case background = "background"
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public enum AppState: String, Codable {
case active = "active"
case background = "background"
}
public enum AppState: String, Codable {
case active
case background
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b1622f2.

@home-assistant home-assistant bot marked this pull request as draft March 11, 2026 08:58
@bgoncal bgoncal requested a review from Copilot March 11, 2026 08:58
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 10 comments.

@bgoncal
Copy link
Member

bgoncal commented Mar 11, 2026

I'm happy to pull auth - but it has been working in my testing - can you help to identify where you're seeing an issue?

I tested in a real device and it is working, so it must have been something with my simulator, let's keep it, but in the future this PR could have been split even in smaller PRs, it would have helped reviewing and avoid a lot of rework on your side.

Regarding the UI of the "block screen" for the authentication (the overlay that prevents user to access kiosk settings), can you make it look like out onboarding permission screens? White (.systemBackground) background and 2 buttons at the bottom? Just to keep some consistency

@davystrong
Copy link

davystrong commented Mar 11, 2026

I've been following this with interest as I'm interested in using an iPad as a home monitor. However, I understood that "Kiosk mode" would mean being able to hide the app's own title bar and menu bar (I guess I mean a fullscreen dashboard). Is this not the case? Any chance this could be added or would that be considered a different feature which should be added as a separate PR?

@bgoncal
Copy link
Member

bgoncal commented Mar 11, 2026

@davystrong this could be added in an iteration PR, frontend already offer a command to "enter" in kiosk mode where it hides visual elements, but this PR is too big already, so let's merge it first. Agree @nstefanelli ?

@bgoncal
Copy link
Member

bgoncal commented Mar 11, 2026

Found one more

Simulator.Screen.Recording.-.iPad.Air.11-inch.M4.-.2026-03-11.at.10.24.53.mov

@bgoncal
Copy link
Member

bgoncal commented Mar 11, 2026

When screensaver is starting it has a weird "blink" before the fade-in

Simulator.Screen.Recording.-.iPad.Air.11-inch.M4.-.2026-03-11.at.10.32.20.mov

@bgoncal
Copy link
Member

bgoncal commented Mar 11, 2026

When access kiosk settings directly through the secret gesture, it is presented in a different sheet size compared to the app settings. App settings sheet size is the one we should follow.

Simulator Screenshot - iPad Air 11-inch (M4) - 2026-03-11 at 10 34 33

@bgoncal
Copy link
Member

bgoncal commented Mar 11, 2026

I would also default the secret gesture to bottom right, since it's where most probably no other element would be positioned

@nstefanelli
Copy link
Author

Agreed — this is planned for a future PR. The kiosk mode frontend integration will allow compatibility with HA's dashboard kiosk modes (hiding sidebar, header, etc.) as a dedicated iteration.

- Merge KioskModeHandler into KioskModeManager per maintainer request;
  manager now holds weak WebViewControllerProtocol reference directly
  and handles screensaver presentation, UI lockdown, and settings modal
- Replace custom 42KIOSK* pbxproj IDs with standard 24-char hex IDs
- Rename loadSettings/saveSettings to settings()/save() per convention
- Remove doc comment from KioskSettingsRecord (redundant with table def)
- Use fetchOne(db, key:) for explicit primary key lookup
- Remove explicit raw values from ScreenState, AppState, ScreenCorner
- Remove .ignoresSafeArea() from clock view (parent VC handles it)
- Fix screensaver blink: configure content before modal presentation
- Use presentOverlayController for consistent settings sheet sizing
- Convert authRequired from @State to computed property (fixes stale state)
- Restyle auth gate overlay to match HA onboarding pattern
- Move clock options into screensaver section
- Use setLocalizedDateFormatFromTemplate for locale-aware date formatting
- Use Current.date() for lastActivityTime init (testability)
- Conditional isIdleTimerDisabled restore in disableKioskMode
- Handle preventAutoLock and screensaverEnabled in settingsDidChange
- Tests assert against L10n values instead of hardcoded English strings
- Remove dead hide() method and unused cancellables
- Default secret exit gesture corner changed to bottomRight
@nstefanelli nstefanelli marked this pull request as ready for review March 12, 2026 00:44
@home-assistant home-assistant bot requested a review from bgoncal March 12, 2026 00:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants