mirror of
https://github.com/home-assistant/iOS.git
synced 2026-02-18 06:26:35 -06:00
<!-- Thank you for submitting a Pull Request and helping to improve Home Assistant. Please complete the following sections to help the processing and review of your changes. Please do not delete anything from this template. --> ## Summary <!-- Provide a brief summary of the changes you have made and most importantly what they aim to achieve --> Recently it was introduced a feature to reload webview when app has been backgrounded for more than 5 minutes, this PR adds a layer on top of it that only reloads if the webview frontend connection state is not connected. ## Screenshots <!-- If this is a user-facing change not in the frontend, please include screenshots in light and dark mode. --> ## Link to pull request in Documentation repository <!-- Pull requests that add, change or remove functionality must have a corresponding pull request in the Companion App Documentation repository (https://github.com/home-assistant/companion.home-assistant). Please add the number of this pull request after the "#" --> Documentation: home-assistant/companion.home-assistant# ## Any other notes <!-- If there is any other information of note, like if this Pull Request is part of a bigger change, please include it here. --> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
234 lines
8.8 KiB
Swift
234 lines
8.8 KiB
Swift
import Foundation
|
|
import PromiseKit
|
|
import Shared
|
|
import UIKit
|
|
import WidgetKit
|
|
|
|
final class WebViewSceneDelegate: NSObject, UIWindowSceneDelegate {
|
|
var window: UIWindow?
|
|
var windowController: WebViewWindowController?
|
|
var urlHandler: IncomingURLHandler?
|
|
|
|
private var updateDatabaseTask: Task<Void, Never>?
|
|
|
|
/// Stores the timestamp when the app enters background, used to determine if auto-refresh is needed on reactivation
|
|
var backgroundTimestamp: Date?
|
|
|
|
/// Time threshold (in seconds) after which WebViewController should refresh when returning from background
|
|
private let backgroundRefreshThreshold: TimeInterval = 5 * 60
|
|
|
|
// swiftlint:disable cyclomatic_complexity
|
|
func scene(
|
|
_ scene: UIScene,
|
|
willConnectTo session: UISceneSession,
|
|
options connectionOptions: UIScene.ConnectionOptions
|
|
) {
|
|
guard let scene = scene as? UIWindowScene else { return }
|
|
// if it tries to connect for an external display, decline -- it'll mirror instead
|
|
guard session.role != .windowExternalDisplay else { return }
|
|
|
|
ScaleFactorMutator.record(sceneIdentifier: session.persistentIdentifier)
|
|
|
|
let window = UIWindow(haScene: scene)
|
|
let windowController = WebViewWindowController(
|
|
window: window,
|
|
restorationActivity: session.stateRestorationActivity
|
|
)
|
|
let urlHandler = IncomingURLHandler(windowController: windowController)
|
|
self.window = window
|
|
self.windowController = windowController
|
|
self.urlHandler = urlHandler
|
|
|
|
with(scene.sizeRestrictions) {
|
|
if scene.traitCollection.userInterfaceIdiom == .mac {
|
|
$0?.minimumSize = CGSize(width: 250, height: 250)
|
|
} else {
|
|
$0?.minimumSize = CGSize(width: 300, height: 300)
|
|
}
|
|
}
|
|
|
|
if Current.isCatalyst, Current.settingsStore.macNativeFeaturesOnly {
|
|
// This getter does not exist on macOS 10.15, so we need to check that it responds.
|
|
// Of course, this is not documented via availability headers, of course.
|
|
if connectionOptions.responds(to: #selector(getter: UIScene.ConnectionOptions.shortcutItem)),
|
|
let shortcutItem = connectionOptions.shortcutItem {
|
|
self.windowScene(scene, performActionFor: shortcutItem, completionHandler: { _ in })
|
|
} else if let url = Current.servers.all.first?.info.connection.activeURL() {
|
|
URLOpener.shared.open(url, options: [:], completionHandler: nil)
|
|
// Close window to avoid empty window left behind
|
|
if let scene = window.windowScene {
|
|
UIApplication.shared.requestSceneSessionDestruction(scene.session, options: nil, errorHandler: nil)
|
|
}
|
|
}
|
|
} else {
|
|
windowController.setup()
|
|
|
|
// This getter does not exist on macOS 10.15, so we need to check that it responds.
|
|
// Of course, this is not documented via availability headers, of course.
|
|
if connectionOptions.responds(to: #selector(getter: UIScene.ConnectionOptions.shortcutItem)),
|
|
let shortcutItem = connectionOptions.shortcutItem {
|
|
self.windowScene(scene, performActionFor: shortcutItem, completionHandler: { _ in })
|
|
}
|
|
}
|
|
#if targetEnvironment(macCatalyst)
|
|
if let titlebar = scene.titlebar {
|
|
// disabling this also disables the "show tab bar" window tab bar (aka not uitabbar)
|
|
titlebar.titleVisibility = .hidden
|
|
titlebar.toolbar = nil
|
|
}
|
|
#endif
|
|
|
|
if !connectionOptions.urlContexts.isEmpty {
|
|
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
|
|
}
|
|
|
|
if !connectionOptions.userActivities.isEmpty {
|
|
for activity in connectionOptions.userActivities {
|
|
self.scene(scene, continue: activity)
|
|
}
|
|
}
|
|
|
|
informManager(from: connectionOptions)
|
|
|
|
#if targetEnvironment(macCatalyst)
|
|
WindowScenesManager.shared.sceneDidBecomeActive(scene)
|
|
#endif
|
|
}
|
|
|
|
func sceneWillResignActive(_ scene: UIScene) {
|
|
#if targetEnvironment(macCatalyst)
|
|
WindowScenesManager.shared.sceneWillResignActive(scene)
|
|
#endif
|
|
|
|
DataWidgetsUpdater.update()
|
|
}
|
|
|
|
func sceneDidDisconnect(_ scene: UIScene) {
|
|
windowController?.clearCachedControllers()
|
|
windowController = nil
|
|
window = nil
|
|
urlHandler = nil
|
|
|
|
#if targetEnvironment(macCatalyst)
|
|
WindowScenesManager.shared.didDiscardScene(scene)
|
|
#endif
|
|
|
|
DataWidgetsUpdater.update()
|
|
}
|
|
|
|
func sceneDidEnterBackground(_ scene: UIScene) {
|
|
if #available(iOS 17.0, *) {
|
|
// if a widget is pending confirmation to execute it's action
|
|
// this will reset that and the widget will be restored to default state
|
|
_ = ResetAllCustomWidgetConfirmationAppIntent()
|
|
}
|
|
DataWidgetsUpdater.update()
|
|
Current.modelManager.unsubscribe()
|
|
Current.appDatabaseUpdater.stop()
|
|
|
|
// Record timestamp when app enters background
|
|
backgroundTimestamp = Current.date()
|
|
}
|
|
|
|
func sceneDidBecomeActive(_ scene: UIScene) {
|
|
updateDatabaseTask?.cancel()
|
|
updateDatabaseTask = Task {
|
|
await updateDatabase()
|
|
}
|
|
cleanWidgetsCache()
|
|
updateLocation()
|
|
|
|
autoRefreshWebViewRoutine()
|
|
}
|
|
|
|
func windowScene(
|
|
_ windowScene: UIWindowScene,
|
|
performActionFor shortcutItem: UIApplicationShortcutItem,
|
|
completionHandler: @escaping (Bool) -> Void
|
|
) {
|
|
urlHandler?.handle(shortcutItem: shortcutItem)
|
|
.done {
|
|
completionHandler(true)
|
|
}.catch { _ in
|
|
completionHandler(false)
|
|
}
|
|
}
|
|
|
|
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
|
for url in URLContexts.map(\.url) {
|
|
urlHandler?.handle(url: url)
|
|
}
|
|
}
|
|
|
|
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
|
|
windowController?.stateRestorationActivity()
|
|
}
|
|
|
|
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
|
urlHandler?.handle(userActivity: userActivity)
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
/// When webview has been inactive for long, when opening the app we reload the webview if it's disconnected
|
|
private func autoRefreshWebViewRoutine() {
|
|
// Check if app was in background for 5 minutes or more
|
|
if let backgroundTimestamp {
|
|
let timeInterval = Current.date().timeIntervalSince(backgroundTimestamp)
|
|
|
|
if timeInterval >= backgroundRefreshThreshold, Current.settingsStore.refreshWebViewAfterInactive {
|
|
// Refresh WebViewController if it exists
|
|
// Note: webViewControllerPromise is a Guarantee, which cannot fail in PromiseKit
|
|
windowController?.webViewControllerPromise.done { webViewController in
|
|
webViewController.refreshIfDisconnected()
|
|
}
|
|
}
|
|
|
|
// Clear the timestamp
|
|
self.backgroundTimestamp = nil
|
|
}
|
|
}
|
|
|
|
/// Whenever a custom widget is executed it can create cache files to hold it state,
|
|
/// this clears it
|
|
private func cleanWidgetsCache() {
|
|
let widgetsCacheFile = AppConstants.widgetsCacheURL
|
|
|
|
// Clean up widgets cache file
|
|
do {
|
|
try FileManager.default.removeItem(at: widgetsCacheFile)
|
|
} catch {
|
|
Current.Log.error("Failed to remove widgets cache file: \(error)")
|
|
}
|
|
}
|
|
|
|
/// Sets up model manager and update database tables for cached panels and entities
|
|
private func updateDatabase() async {
|
|
Current.modelManager.cleanup().cauterize()
|
|
Current.modelManager.subscribe(isAppInForeground: {
|
|
UIApplication.shared.applicationState == .active
|
|
})
|
|
}
|
|
|
|
/// Force update location when user opens the app
|
|
private func updateLocation() {
|
|
Current.location.oneShotLocation(.Launch, nil).pipe { result in
|
|
switch result {
|
|
case let .fulfilled(location):
|
|
for api in Current.apis {
|
|
api.SubmitLocation(updateType: .Launch, location: location, zone: nil).pipe { result in
|
|
switch result {
|
|
case .fulfilled:
|
|
break // Submission succeeded, no action needed
|
|
case let .rejected(error):
|
|
Current.Log.error("Failed to submit location: \(error)")
|
|
}
|
|
}
|
|
}
|
|
case let .rejected(error):
|
|
Current.Log.error("Failed to get location on sceneDidBecomeActive: \(error)")
|
|
}
|
|
}
|
|
}
|
|
}
|