Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Sources/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
title: L10n.Updater.UpdateAvailable.open(update.name),
style: .default,
handler: { _ in
UIApplication.shared.open(update.htmlUrl, options: [:], completionHandler: nil)
Current.urlOpener.open(update.htmlUrl, options: [:], completionHandler: nil)
}
))
alert.addAction(UIAlertAction(title: L10n.okLabel, style: .cancel, handler: nil))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ final class OnboardingPermissionsNavigationViewModel: NSObject, ObservableObject
case .denied, .restricted:
// Open iOS settings for user to manually enable location
if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsUrl)
Current.urlOpener.open(settingsUrl, options: [:], completionHandler: nil)
}
case .authorizedWhenInUse, .authorizedAlways:
// Permission already granted, apply the context-specific needs
Expand Down
2 changes: 1 addition & 1 deletion Sources/App/Scenes/WebViewSceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ final class WebViewSceneDelegate: NSObject, UIWindowSceneDelegate {
let shortcutItem = connectionOptions.shortcutItem {
self.windowScene(scene, performActionFor: shortcutItem, completionHandler: { _ in })
} else if let url = Current.servers.all.first?.info.connection.activeURL() {
UIApplication.shared.open(url)
Current.urlOpener.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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ class ConnectionSettingsViewController: HAFormViewController, RowControllerType
@objc private func activateServerTapped() {
if Current.isCatalyst, Current.settingsStore.macNativeFeaturesOnly {
if let url = server.info.connection.activeURL() {
UIApplication.shared.open(url)
Current.urlOpener.open(url, options: [:], completionHandler: nil)
}
} else {
Current.sceneManager.webViewWindowControllerPromise.done {
Expand Down
2 changes: 1 addition & 1 deletion Sources/App/Settings/DebugView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ struct DebugView: View {
logsURL = url
if Current.isCatalyst {
if let url = Current.Log.archiveURL() {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
Current.urlOpener.open(url, options: [:], completionHandler: nil)
}
} else {
loadingLogs = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class NotificationSoundsViewController: HAFormViewController, UIDocumentPickerDe
$0.title = L10n.SettingsDetails.Notifications.Sounds.importMacOpenFolder
$0.onCellSelection { _, _ in
do {
try UIApplication.shared.open(self.librarySoundsURL(), options: [:], completionHandler: nil)
try Current.urlOpener.open(self.librarySoundsURL(), options: [:], completionHandler: nil)
} catch {
Current.Log.error("couldn't open folder: \(error)")
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/App/Settings/SettingsDetailViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ class SettingsDetailViewController: HAFormViewController, TypedRowControllerType
$0.accessoryIcon = .openInNewIcon
$0.onCellSelection { _, row in
guard let url = URL(string: "https://companion.home-assistant.io/docs/core/actions/") else { return }
UIApplication.shared.open(url)
Current.urlOpener.open(url, options: [:], completionHandler: nil)
row.deselect(animated: true)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ extension UIApplication {

func openSettings(destination: OpenSettingsDestination, completionHandler: ((Bool) -> Void)? = nil) {
if let url = destination.url {
UIApplication.shared.open(url, options: [:], completionHandler: completionHandler)
Current.urlOpener.open(url, options: [:], completionHandler: completionHandler)
} else {
completionHandler?(false)
}
Expand Down
10 changes: 5 additions & 5 deletions Sources/App/Utilities/OpenInChromeController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ open class OpenInChromeController {
open func isChromeInstalled() -> Bool {
if let simpleURL = URL(string: googleChromeHTTPScheme),
let callbackURL = URL(string: googleChromeCallbackScheme) {
return UIApplication.shared.canOpenURL(simpleURL) || UIApplication.shared.canOpenURL(callbackURL)
return Current.urlOpener.canOpenURL(simpleURL) || Current.urlOpener.canOpenURL(callbackURL)
}
return false
}
Expand All @@ -63,7 +63,7 @@ open class OpenInChromeController {
open func openInChrome(_ url: URL, callbackURL: URL?, createNewTab: Bool = false) {
if let chromeSimpleURL = URL(string: googleChromeHTTPScheme),
let chromeCallbackURL = URL(string: googleChromeCallbackScheme) {
if UIApplication.shared.canOpenURL(chromeCallbackURL) {
if Current.urlOpener.canOpenURL(chromeCallbackURL) {
var appName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String
// CFBundleDisplayName is an optional key, so we will use CFBundleName if it does not exist
if appName == nil {
Expand All @@ -87,10 +87,10 @@ open class OpenInChromeController {
chromeURLString += "&create-new-tab"
}
if let chromeURL = URL(string: chromeURLString) {
UIApplication.shared.open(chromeURL, options: [:], completionHandler: nil)
Current.urlOpener.open(chromeURL, options: [:], completionHandler: nil)
}
}
} else if UIApplication.shared.canOpenURL(chromeSimpleURL) {
} else if Current.urlOpener.canOpenURL(chromeSimpleURL) {
let scheme = url.scheme?.lowercased()
var chromeScheme: String?
if scheme == "http" {
Expand All @@ -102,7 +102,7 @@ open class OpenInChromeController {
let absoluteURLString = url.absoluteString
let lowerBound = absoluteURLString.range(of: ":")!.lowerBound
let chromeURLString = chromeScheme + absoluteURLString.suffix(from: lowerBound)
UIApplication.shared.open(URL(string: chromeURLString)!, options: [:], completionHandler: nil)
Current.urlOpener.open(URL(string: chromeURLString)!, options: [:], completionHandler: nil)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/App/Utilities/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func resetStores() {

func openURLInBrowser(_ urlToOpen: URL, _ sender: UIViewController?) {
guard ["http", "https"].contains(urlToOpen.scheme?.lowercased()) else {
UIApplication.shared.open(urlToOpen, options: [:], completionHandler: nil)
Current.urlOpener.open(urlToOpen, options: [:], completionHandler: nil)
return
}

Expand All @@ -42,7 +42,7 @@ func openURLInBrowser(_ urlToOpen: URL, _ sender: UIViewController?) {
let sfv = SFSafariViewController(url: urlToOpen)
sender!.present(sfv, animated: true)
default:
UIApplication.shared.open(urlToOpen, options: [:], completionHandler: nil)
Current.urlOpener.open(urlToOpen, options: [:], completionHandler: nil)
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/App/WebView/DownloadManager/DownloadManagerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ struct DownloadManagerView: View {
}
.onChange(of: viewModel.finished) { _, newValue in
if newValue, Current.isCatalyst {
UIApplication.shared.open(AppConstants.DownloadsDirectory)
Current.urlOpener.open(AppConstants.DownloadsDirectory, options: [:], completionHandler: nil)
}
}
}
Expand Down Expand Up @@ -83,7 +83,7 @@ struct DownloadManagerView: View {
if let url = viewModel.lastURLCreated {
if Current.isCatalyst {
Button {
UIApplication.shared.open(AppConstants.DownloadsDirectory)
Current.urlOpener.open(AppConstants.DownloadsDirectory, options: [:], completionHandler: nil)
} label: {
Label(viewModel.fileName, systemSymbol: .folder)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/App/WebView/Views/ConnectionErrorDetailsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ struct ConnectionErrorDetailsView: View {
tint: .haPrimary
) {
if Current.isCatalyst, let logsURL = Current.Log.archiveURL() {
UIApplication.shared.open(logsURL)
Current.urlOpener.open(logsURL, options: [:], completionHandler: nil)
} else {
showExportLogsShareSheet = true
feedbackGenerator.notificationOccurred(.success)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ struct ConnectionSecurityLevelBlockView: View {
case .notDetermined:
Current.Log.info("Location permission not determined")
case .denied, .restricted:
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
Current.urlOpener.open(
URL(string: UIApplication.openSettingsURLString)!,
options: [:],
completionHandler: nil
)
case .authorizedWhenInUse, .authorizedAlways:
// Handle permission change - reload requirements to update UI
viewModel.loadRequirements()
Expand Down
4 changes: 2 additions & 2 deletions Sources/App/WebView/WebViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,7 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
urlComponents.queryItems = urlComponents.queryItems?.filter { $0.name != "external_auth" }

if let url = urlComponents.url {
UIApplication.shared.open(url)
Current.urlOpener.open(url, options: [:], completionHandler: nil)
}
}
}
Expand Down Expand Up @@ -914,7 +914,7 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
buttonImage: nil,
buttonTitle: L10n.openLabel,
buttonTapHandler: { _ in
UIApplication.shared.open(alert.url, options: [:], completionHandler: nil)
Current.urlOpener.open(alert.url, options: [:], completionHandler: nil)
SwiftMessages.hide()
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ struct OpenEntityAppIntent: AppIntent {
serverId: entity.serverId
) {
DispatchQueue.main.async {
UIApplication.shared.open(url)
Current.urlOpener.open(url, options: [:], completionHandler: nil)
}
} else if let url =
AppConstants.openEntityDeeplinkURL(entityId: entity.entityId, serverId: entity.serverId) {
DispatchQueue.main.async {
UIApplication.shared.open(url)
Current.urlOpener.open(url, options: [:], completionHandler: nil)
}
}
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ struct OpenPageAppIntent: AppIntent {
serverId: server.identifier.rawValue
) {
DispatchQueue.main.async {
UIApplication.shared.open(url)
Current.urlOpener.open(url, options: [:], completionHandler: nil)
}
}
#endif
Expand Down
3 changes: 3 additions & 0 deletions Sources/Shared/Environment/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -486,4 +486,7 @@
public var appSessionValues: AppSessionValuesProtocol = AppSessionValues.shared

public var locationManager: LocationManagerProtocol = LocationManager()

/// Provides a way to open URLs and check if URLs can be opened.
public var urlOpener: URLOpenerServiceProtocol = URLOpenerServiceImpl()

Check failure on line 491 in Sources/Shared/Environment/Environment.swift

View workflow job for this annotation

GitHub Actions / test

cannot find type 'URLOpenerServiceProtocol' in scope

Check failure on line 491 in Sources/Shared/Environment/Environment.swift

View workflow job for this annotation

GitHub Actions / test

cannot find type 'URLOpenerServiceProtocol' in scope
}
40 changes: 40 additions & 0 deletions Sources/Shared/Services/URLOpenerService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation
import UIKit

public protocol URLOpenerServiceProtocol {
@discardableResult
func open(
_ url: URL,
options: [UIApplication.OpenExternalURLOptionsKey: Any],
completionHandler: ((Bool) -> Void)?
) -> Bool

func canOpenURL(_ url: URL) -> Bool
}

public final class URLOpenerServiceImpl: URLOpenerServiceProtocol {
public init() {}

@discardableResult
public func open(
_ url: URL,
options: [UIApplication.OpenExternalURLOptionsKey: Any],
completionHandler: ((Bool) -> Void)?
) -> Bool {
#if os(iOS)
UIApplication.shared.open(url, options: options, completionHandler: completionHandler)
return true
#else
completionHandler?(false)
return false
#endif
}

public func canOpenURL(_ url: URL) -> Bool {
#if os(iOS)
return UIApplication.shared.canOpenURL(url)
#else
return false
#endif
}
}
46 changes: 46 additions & 0 deletions Tests/Shared/URLOpenerService.test.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Foundation
@testable import Shared
import XCTest

class URLOpenerServiceTests: XCTestCase {
func testProtocolExists() {
// Test that the protocol can be used
let service: URLOpenerServiceProtocol = URLOpenerServiceImpl()
XCTAssertNotNil(service)
}

func testCanOpenURL() {
let service = URLOpenerServiceImpl()

// Test with a valid URL scheme
if let url = URL(string: "https://www.apple.com") {
// We can't actually test the return value in unit tests without a real UIApplication
// but we can verify the method exists and doesn't crash
_ = service.canOpenURL(url)
}
}

func testOpenURL() {
let service = URLOpenerServiceImpl()

if let url = URL(string: "https://www.apple.com") {
// In unit tests, this will be called but won't actually open anything
// We just verify it doesn't crash
service.open(url, options: [:], completionHandler: nil)
}
}

func testOpenURLWithCompletion() {
let service = URLOpenerServiceImpl()
let expectation = expectation(description: "Completion handler called")

if let url = URL(string: "https://www.apple.com") {
service.open(url, options: [:]) { _ in
// In unit tests this may or may not succeed depending on environment
expectation.fulfill()
}

waitForExpectations(timeout: 1.0)
}
}
}
Loading