Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions HomeAssistant.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@
425573D12B5576E600145217 /* CarPlayDomainsListTemplate+Build.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425573D02B5576E600145217 /* CarPlayDomainsListTemplate+Build.swift */; };
425573D32B55770D00145217 /* CarPlayDomainsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425573D22B55770D00145217 /* CarPlayDomainsListViewModel.swift */; };
425573DA2B57DDE000145217 /* WindowScenesManager.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425573D92B57DDE000145217 /* WindowScenesManager.test.swift */; };
52386BFCFAFCBE783D0BCDD9 /* MacBrowserSceneLauncher.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26BE2F311BDB2F205F7432FF /* MacBrowserSceneLauncher.test.swift */; };
425573EB2B588FFB00145217 /* CarPlayListItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425573EA2B588FFB00145217 /* CarPlayListItemProvider.swift */; };
425573ED2B58904000145217 /* CarPlayEntityListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425573EC2B58904000145217 /* CarPlayEntityListItem.swift */; };
4256A70D2F5ECBB3003D464F /* SpeechTranscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4256A70C2F5ECBB3003D464F /* SpeechTranscriber.swift */; };
Expand Down Expand Up @@ -1690,6 +1691,7 @@
DA6F4C18D66EDBA5DCEAE833 /* DatabaseMigration.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892F0EF22A0B9F20AAEE4CCA /* DatabaseMigration.test.swift */; };
DB54626ADCE0C32094C8C0B9 /* LoadingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BFD0D30836C69840AB63A8A /* LoadingButton.swift */; };
DE8048842D9FB6C90459E229 /* QuickActionWindowSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1948705C52B43ACD36FD0B32 /* QuickActionWindowSceneDelegate.swift */; };
1A693E34211ADFAB76155607 /* MacBrowserSceneLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 730D7B17F8A2B19BCA181F91 /* MacBrowserSceneLauncher.swift */; };
DFB59CC17BC47B8A6949B81C /* KioskScreensaverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71373DD7455D9CE893EB450B /* KioskScreensaverView.swift */; };
E33F645682064B22AC4CDFBB /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = C6ACD1840F0E4F5FBFDDE88B /* SFSafeSymbols */; };
E3A02409794174F002C8BB4F /* IconSearchPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC23DE131CA8813C2DBD657 /* IconSearchPicker.swift */; };
Expand Down Expand Up @@ -2346,6 +2348,7 @@
1861EB0361816DC9260D1F5E /* ComplicationFamilySelectView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ComplicationFamilySelectView.swift; sourceTree = "<group>"; };
1945E8E386664E5A88668E77 /* WebViewController+ReAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebViewController+ReAuth.swift"; sourceTree = "<group>"; };
1948705C52B43ACD36FD0B32 /* QuickActionWindowSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickActionWindowSceneDelegate.swift; sourceTree = "<group>"; };
730D7B17F8A2B19BCA181F91 /* MacBrowserSceneLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacBrowserSceneLauncher.swift; sourceTree = "<group>"; };
1CCF41D72CA895DB501333EF /* KioskModeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KioskModeManager.swift; sourceTree = "<group>"; };
207E35C8F1554A9AD616FFA2 /* Pods-iOS-Extensions-Share-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-iOS-Extensions-Share-metadata.plist"; path = "Pods/Pods-iOS-Extensions-Share-metadata.plist"; sourceTree = "<group>"; };
208A5362BE60377368ACB1D6 /* RealmResultsObserver.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RealmResultsObserver.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2584,6 +2587,7 @@
425573D02B5576E600145217 /* CarPlayDomainsListTemplate+Build.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarPlayDomainsListTemplate+Build.swift"; sourceTree = "<group>"; };
425573D22B55770D00145217 /* CarPlayDomainsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayDomainsListViewModel.swift; sourceTree = "<group>"; };
425573D92B57DDE000145217 /* WindowScenesManager.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowScenesManager.test.swift; sourceTree = "<group>"; };
26BE2F311BDB2F205F7432FF /* MacBrowserSceneLauncher.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacBrowserSceneLauncher.test.swift; sourceTree = "<group>"; };
425573E42B58380D00145217 /* CarPlay.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CarPlay.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.sdk/System/iOSSupport/System/Library/Frameworks/CarPlay.framework; sourceTree = DEVELOPER_DIR; };
425573EA2B588FFB00145217 /* CarPlayListItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayListItemProvider.swift; sourceTree = "<group>"; };
425573EC2B58904000145217 /* CarPlayEntityListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayEntityListItem.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4525,6 +4529,7 @@
118261F424F8C7C1000795C6 /* SceneManager.swift */,
FD3BC66229B9FF8F00B19FBE /* CarPlaySceneDelegate.swift */,
1948705C52B43ACD36FD0B32 /* QuickActionWindowSceneDelegate.swift */,
730D7B17F8A2B19BCA181F91 /* MacBrowserSceneLauncher.swift */,
42F1DA5A2B4BF7DF002729BC /* WindowSizeObserver.swift */,
42F1DA5C2B4BF85F002729BC /* WindowScenesManager.swift */,
);
Expand Down Expand Up @@ -4687,6 +4692,7 @@
isa = PBXGroup;
children = (
425573D92B57DDE000145217 /* WindowScenesManager.test.swift */,
26BE2F311BDB2F205F7432FF /* MacBrowserSceneLauncher.test.swift */,
);
path = Scenes;
sourceTree = "<group>";
Expand Down Expand Up @@ -9680,6 +9686,7 @@
11195F6F267EFC8E003DF674 /* NotificationManagerLocalPushInterfaceDirect.swift in Sources */,
FD3BC66329B9FF8F00B19FBE /* CarPlaySceneDelegate.swift in Sources */,
DE8048842D9FB6C90459E229 /* QuickActionWindowSceneDelegate.swift in Sources */,
1A693E34211ADFAB76155607 /* MacBrowserSceneLauncher.swift in Sources */,
11C4629424B189B100031902 /* NotificationRateLimitsAPI.swift in Sources */,
11A71C6B24A463FC00D9565F /* ZoneManagerState.swift in Sources */,
42106F562F447C6B009351FC /* MTLSLabsLabel.swift in Sources */,
Expand Down Expand Up @@ -9988,6 +9995,7 @@
42ACC2AF2E9FA1D60045A3FD /* DiscoveredHomeAssistant.test.swift in Sources */,
117D8A0A24A9381F00580913 /* UIColor+CSSRGB.test.swift in Sources */,
425573DA2B57DDE000145217 /* WindowScenesManager.test.swift in Sources */,
52386BFCFAFCBE783D0BCDD9 /* MacBrowserSceneLauncher.test.swift in Sources */,
11ED43A027279AFA00B5FD45 /* OnboardingAuthLoginImpl.test.swift in Sources */,
11EFD3BE27253504000AF78B /* OnboardingAuthStepConnectivity.test.swift in Sources */,
42E3B8C02D8ACE0400F5D084 /* OptionalsDefaultTests.swift in Sources */,
Expand Down
31 changes: 31 additions & 0 deletions Sources/App/Scenes/MacBrowserSceneLauncher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Shared

/// Gates the Mac "Open Home Assistant UI in browser" (`macNativeFeaturesOnly`) auto-launch that runs when a
/// `"WebView"` scene connects (see `QuickActionWindowSceneDelegate`).
///
/// Only the *first* scene connection of the process — the user's cold launch — should open Home Assistant in
/// the browser and destroy the otherwise-empty window. macOS reconnects this scene in the background throughout
/// the app's lifetime (for state restoration and other background work), and every reconnection used to run the
/// browser auto-launch, so the dashboard reopened every few minutes even though the user never asked for it
/// (#4985). Later, genuinely user-initiated opens while the app is already running are handled elsewhere — the
/// Dock reopen handler and the status-item menu both go through `StatusItemPrimaryAction.openInBrowserIfNeeded()`
/// — so this scene-connection path only ever needs to cover the cold launch.
enum MacBrowserSceneLauncher {
/// Whether the initial (cold-launch) `"WebView"` scene connection has already been observed this process.
/// Reset only implicitly, by the process being relaunched.
static var didHandleInitialSceneConnection = false

/// Records that a `"WebView"` scene has connected and reports whether this was the first connection of the
/// process (i.e. the user's cold launch). Subsequent calls return `false`.
@discardableResult
static func markSceneConnected() -> Bool {
let isInitialConnection = !didHandleInitialSceneConnection
didHandleInitialSceneConnection = true
return isInitialConnection
Comment on lines +14 to +24
}

/// Whether the "Open Home Assistant UI in browser" behaviour applies on this platform/configuration.
static var isBrowserLaunchEnabled: Bool {
Current.isCatalyst && Current.settingsStore.macNativeFeaturesOnly
}
}
11 changes: 9 additions & 2 deletions Sources/App/Scenes/QuickActionWindowSceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ final class QuickActionWindowSceneDelegate: UIResponder, UIWindowSceneDelegate {
) {
guard let windowScene = scene as? UIWindowScene else { return }

// Record this connection and remember whether it was the process's first (cold-launch) one. macOS
// reconnects this scene in the background throughout the app's lifetime, so only the first connection
// represents a user-initiated launch (see `MacBrowserSceneLauncher`).
let isInitialSceneConnection = MacBrowserSceneLauncher.markSceneConnected()

// Cold launch via a quick action: the app was launched by tapping an app-icon shortcut.
// This getter does not exist on macOS 10.15, so check that it responds before accessing it.
if connectionOptions.responds(to: #selector(getter: UIScene.ConnectionOptions.shortcutItem)),
Expand All @@ -38,8 +43,10 @@ final class QuickActionWindowSceneDelegate: UIResponder, UIWindowSceneDelegate {
// "Open Home Assistant UI in browser" (Mac): when the app is opened by tapping its icon, open
// Home Assistant in the default browser and destroy the empty webview window so none is left behind.
// This only runs on macOS, where the last-known network information is read live from macBridge,
// so the synchronous evaluation is current.
if Current.isCatalyst, Current.settingsStore.macNativeFeaturesOnly,
// so the synchronous evaluation is current. Only the initial (cold-launch) connection triggers this;
// later background scene reconnections must not reopen the browser (#4985). Subsequent user-initiated
// opens are handled by the Dock reopen handler / status-item menu via `StatusItemPrimaryAction`.
if isInitialSceneConnection, MacBrowserSceneLauncher.isBrowserLaunchEnabled,
let url = Current.servers.all.first?.activeURLUsingLastKnownNetworkState() {
URLOpener.shared.open(url, options: [:], completionHandler: nil)
UIApplication.shared.requestSceneSessionDestruction(session, options: nil, errorHandler: nil)
Expand Down
52 changes: 52 additions & 0 deletions Tests/App/Scenes/MacBrowserSceneLauncher.test.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
@testable import HomeAssistant
@testable import Shared
import XCTest

final class MacBrowserSceneLauncherTests: XCTestCase {
private var originalIsCatalyst: Bool!
private var originalMacNativeFeaturesOnly: Bool!

override func setUpWithError() throws {
try super.setUpWithError()
originalIsCatalyst = Current.isCatalyst
originalMacNativeFeaturesOnly = Current.settingsStore.macNativeFeaturesOnly
MacBrowserSceneLauncher.didHandleInitialSceneConnection = false
}

override func tearDownWithError() throws {
Current.isCatalyst = originalIsCatalyst
Current.settingsStore.macNativeFeaturesOnly = originalMacNativeFeaturesOnly
MacBrowserSceneLauncher.didHandleInitialSceneConnection = false
try super.tearDownWithError()
}
Comment on lines +6 to +21

func testMarkSceneConnectedReturnsTrueOnlyForInitialConnection() {
XCTAssertTrue(MacBrowserSceneLauncher.markSceneConnected())
XCTAssertFalse(MacBrowserSceneLauncher.markSceneConnected())
XCTAssertFalse(MacBrowserSceneLauncher.markSceneConnected())
}

func testMarkSceneConnectedSetsHandledFlag() {
XCTAssertFalse(MacBrowserSceneLauncher.didHandleInitialSceneConnection)
MacBrowserSceneLauncher.markSceneConnected()
XCTAssertTrue(MacBrowserSceneLauncher.didHandleInitialSceneConnection)
}

func testBrowserLaunchEnabledRequiresCatalystAndPreference() {
Current.isCatalyst = true
Current.settingsStore.macNativeFeaturesOnly = true
XCTAssertTrue(MacBrowserSceneLauncher.isBrowserLaunchEnabled)
}

func testBrowserLaunchDisabledWhenPreferenceOff() {
Current.isCatalyst = true
Current.settingsStore.macNativeFeaturesOnly = false
XCTAssertFalse(MacBrowserSceneLauncher.isBrowserLaunchEnabled)
}

func testBrowserLaunchDisabledWhenNotCatalyst() {
Current.isCatalyst = false
Current.settingsStore.macNativeFeaturesOnly = true
XCTAssertFalse(MacBrowserSceneLauncher.isBrowserLaunchEnabled)
}
}
Loading