diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index cf86ddfec..7caf12090 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -192,6 +192,7 @@ 1C8BC70A18060677E295A846 /* ShareToMapsAppActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */; }; 1C9BB74711E5F24C77B7FED0 /* RoomMembersListScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */; }; 1CA094038D4D036A6F0A1314 /* VoiceMessageTrashButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABF84AA68B2B7584D9275769 /* VoiceMessageTrashButton.swift */; }; + 1D2D713A5A269072D103AE37 /* CLLocationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA46D6DD4B4AB17E1D45092E /* CLLocationManagerProtocol.swift */; }; 1D5DC685CED904386C89B7DA /* NSRegularExpresion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */; }; 1D623953F970D11F6F38499C /* AppLockService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851B95BB98649B8E773D6790 /* AppLockService.swift */; }; 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */; }; @@ -624,6 +625,7 @@ 69DE29C3E3180BB17D840690 /* ProgressCursorModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97C8E13A1FBA717B0C277ECC /* ProgressCursorModifier.swift */; }; 6A38D0A2BC3943A92D82576E /* EditRoomAddressScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B18089ED50324583BB2FB7 /* EditRoomAddressScreenViewModelProtocol.swift */; }; 6A54F52443EC52AC5CD772C0 /* JoinRoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 869A8A4632E511351BFE2EC4 /* JoinRoomScreen.swift */; }; + 6A5FDF9306CBD62C7EDDB552 /* CLLocationManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C723327DC4A3093CD9675B27 /* CLLocationManagerMock.swift */; }; 6A64546ABE648ED9E6DBB459 /* RemoteSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D186A6DB8FAC5C9D0E4D61 /* RemoteSettingsHook.swift */; }; 6AB306367E56A6F6DFA0E2FF /* RoomSummaryProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46E441BA50705E6CEC89FE0 /* RoomSummaryProviderTests.swift */; }; 6AD722DD92E465E56D2885AB /* BugReportScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */; }; @@ -2706,6 +2708,7 @@ C6A9F49B3EE59147AF2F70BB /* SeparatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineItem.swift; sourceTree = ""; }; C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = ""; }; C715CFE00686DACA59D836EA /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/SAS.strings; sourceTree = ""; }; + C723327DC4A3093CD9675B27 /* CLLocationManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocationManagerMock.swift; sourceTree = ""; }; C729D95CB4588D4D9AAC3DFA /* RoomChangePermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenModels.swift; sourceTree = ""; }; C75EF87651B00A176AB08E97 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C75FE3F524B575D53787868C /* TimelineMediaPreviewRedactConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewRedactConfirmationView.swift; sourceTree = ""; }; @@ -2810,6 +2813,7 @@ D9C5AA3EF7EC67C01C75CEDD /* LabsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsScreen.swift; sourceTree = ""; }; DA14564EE143F73F7E4D1F79 /* RoomNotificationSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenModels.swift; sourceTree = ""; }; DA3D82522494E78746B2214E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/SAS.strings; sourceTree = ""; }; + DA46D6DD4B4AB17E1D45092E /* CLLocationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocationManagerProtocol.swift; sourceTree = ""; }; DAB8D7926A5684E18196B538 /* VoiceMessageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCache.swift; sourceTree = ""; }; DADECBBB672497BCD4822468 /* Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreen.swift; sourceTree = ""; }; @@ -3730,6 +3734,7 @@ 8F7FC9580CABF797A2E6213A /* BugReportServiceMock.swift */, 633BAD3D9BB44B2AED7CBB93 /* ClassicAppManagerMock.swift */, E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */, + C723327DC4A3093CD9675B27 /* CLLocationManagerMock.swift */, 4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */, 86E1BAA7232081635662A83F /* CXProviderMock.swift */, E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */, @@ -5454,6 +5459,7 @@ 90FD376E9373246E4925CE95 /* Location */ = { isa = PBXGroup; children = ( + DA46D6DD4B4AB17E1D45092E /* CLLocationManagerProtocol.swift */, D17F49E39CC38DAB7B305701 /* LiveLocationManager.swift */, 33752AE856E93CE62412B7A1 /* LiveLocationManagerProtocol.swift */, ); @@ -8120,6 +8126,8 @@ 9A8E6FCD86B89970EC72EFD8 /* BugReportServiceMock.swift in Sources */, 172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */, E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */, + 6A5FDF9306CBD62C7EDDB552 /* CLLocationManagerMock.swift in Sources */, + 1D2D713A5A269072D103AE37 /* CLLocationManagerProtocol.swift in Sources */, 5470E62F65AE1803BBF3D528 /* CXProviderMock.swift in Sources */, 3D0DAED550E967AB49F1758C /* CXProviderProtocol.swift in Sources */, 01B63F1A04A276B39AC17014 /* CallInviteRoomTimelineItem.swift in Sources */, diff --git a/ElementX/Sources/Mocks/CLLocationManagerMock.swift b/ElementX/Sources/Mocks/CLLocationManagerMock.swift new file mode 100644 index 000000000..b86eb1929 --- /dev/null +++ b/ElementX/Sources/Mocks/CLLocationManagerMock.swift @@ -0,0 +1,20 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import CoreLocation + +extension CLLocationManagerMock { + struct Configuration { + var authorizationStatus: CLAuthorizationStatus = .authorizedAlways + } + + convenience init(_ configuration: Configuration) { + self.init() + + underlyingAuthorizationStatus = configuration.authorizationStatus + } +} diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index bcdd53189..b97d0aa43 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2187,6 +2187,65 @@ class BugReportServiceMock: BugReportServiceProtocol, @unchecked Sendable { } } } +class CLLocationManagerMock: CLLocationManagerProtocol, @unchecked Sendable { + weak var delegate: CLLocationManagerDelegate? + var allowsBackgroundLocationUpdates: Bool { + get { return underlyingAllowsBackgroundLocationUpdates } + set(value) { underlyingAllowsBackgroundLocationUpdates = value } + } + var underlyingAllowsBackgroundLocationUpdates: Bool! + var desiredAccuracy: CLLocationAccuracy { + get { return underlyingDesiredAccuracy } + set(value) { underlyingDesiredAccuracy = value } + } + var underlyingDesiredAccuracy: CLLocationAccuracy! + var pausesLocationUpdatesAutomatically: Bool { + get { return underlyingPausesLocationUpdatesAutomatically } + set(value) { underlyingPausesLocationUpdatesAutomatically = value } + } + var underlyingPausesLocationUpdatesAutomatically: Bool! + var authorizationStatus: CLAuthorizationStatus { + get { return underlyingAuthorizationStatus } + set(value) { underlyingAuthorizationStatus = value } + } + var underlyingAuthorizationStatus: CLAuthorizationStatus! + + //MARK: - requestAlwaysAuthorization + + var requestAlwaysAuthorizationUnderlyingCallsCount = 0 + var requestAlwaysAuthorizationCallsCount: Int { + get { + if Thread.isMainThread { + return requestAlwaysAuthorizationUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = requestAlwaysAuthorizationUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + requestAlwaysAuthorizationUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + requestAlwaysAuthorizationUnderlyingCallsCount = newValue + } + } + } + } + var requestAlwaysAuthorizationCalled: Bool { + return requestAlwaysAuthorizationCallsCount > 0 + } + var requestAlwaysAuthorizationClosure: (() -> Void)? + + func requestAlwaysAuthorization() { + requestAlwaysAuthorizationCallsCount += 1 + requestAlwaysAuthorizationClosure?() + } +} class CXProviderMock: CXProviderProtocol, @unchecked Sendable { //MARK: - setDelegate diff --git a/ElementX/Sources/Services/Location/CLLocationManagerProtocol.swift b/ElementX/Sources/Services/Location/CLLocationManagerProtocol.swift new file mode 100644 index 000000000..0aa60812f --- /dev/null +++ b/ElementX/Sources/Services/Location/CLLocationManagerProtocol.swift @@ -0,0 +1,21 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import CoreLocation + +// sourcery: AutoMockable +protocol CLLocationManagerProtocol: AnyObject { + var delegate: CLLocationManagerDelegate? { get set } + var allowsBackgroundLocationUpdates: Bool { get set } + var desiredAccuracy: CLLocationAccuracy { get set } + var pausesLocationUpdatesAutomatically: Bool { get set } + var authorizationStatus: CLAuthorizationStatus { get } + + func requestAlwaysAuthorization() +} + +extension CLLocationManager: CLLocationManagerProtocol { } diff --git a/ElementX/Sources/Services/Location/LiveLocationManager.swift b/ElementX/Sources/Services/Location/LiveLocationManager.swift index 22d26c76f..7f6efaac4 100644 --- a/ElementX/Sources/Services/Location/LiveLocationManager.swift +++ b/ElementX/Sources/Services/Location/LiveLocationManager.swift @@ -10,7 +10,7 @@ import CoreLocation class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationManagerDelegate { private let clientProxy: ClientProxyProtocol - private let locationManager: CLLocationManager + private let locationManager: CLLocationManagerProtocol private let appSettings: AppSettings private let authorizationStatusSubject: CurrentValueSubject @@ -30,21 +30,22 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana @MainActor init(clientProxy: ClientProxyProtocol, - appSettings: AppSettings) { + appSettings: AppSettings, + locationManager: @autoclosure @MainActor () -> CLLocationManagerProtocol = CLLocationManager()) { self.clientProxy = clientProxy self.appSettings = appSettings // Very important, the CLLocationManager needs to be initialised on the main thread // or the delegate functions won't be handled! // https://developer.apple.com/documentation/corelocation/cllocationmanagerdelegate - locationManager = CLLocationManager() - authorizationStatusSubject = CurrentValueSubject(locationManager.authorizationStatus) + self.locationManager = locationManager() + authorizationStatusSubject = CurrentValueSubject(self.locationManager.authorizationStatus) super.init() - locationManager.delegate = self - locationManager.allowsBackgroundLocationUpdates = true - locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters - locationManager.pausesLocationUpdatesAutomatically = false + self.locationManager.delegate = self + self.locationManager.allowsBackgroundLocationUpdates = true + self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters + self.locationManager.pausesLocationUpdatesAutomatically = false setupSubscriptions() } diff --git a/UnitTests/Sources/LiveLocationManagerTests.swift b/UnitTests/Sources/LiveLocationManagerTests.swift index d323cd9d8..bc9f34194 100644 --- a/UnitTests/Sources/LiveLocationManagerTests.swift +++ b/UnitTests/Sources/LiveLocationManagerTests.swift @@ -12,6 +12,7 @@ import Testing @MainActor final class LiveLocationManagerTests { private var clientProxy: ClientProxyMock! + private var locationManagerMock: CLLocationManagerMock! private var manager: LiveLocationManager! private let appSettings: AppSettings @@ -20,7 +21,8 @@ final class LiveLocationManagerTests { AppSettings.resetAllSettings() appSettings = AppSettings() clientProxy = ClientProxyMock(.init()) - manager = LiveLocationManager(clientProxy: clientProxy, appSettings: appSettings) + locationManagerMock = CLLocationManagerMock(.init()) + manager = LiveLocationManager(clientProxy: clientProxy, appSettings: appSettings, locationManager: locationManagerMock) } deinit {