From dd7048d5c2d209b9535634d2376b87b9126152e7 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Mon, 20 Apr 2026 23:23:04 +0200 Subject: [PATCH] handle reduced accuracy authorization case --- .../Sources/Mocks/CLLocationManagerMock.swift | 2 + .../Mocks/Generated/GeneratedMocks.swift | 75 +++++++++++++++++++ .../Location/CLLocationManagerProtocol.swift | 3 + .../Location/LiveLocationManager.swift | 54 +++++++++---- .../Sources/LiveLocationManagerTests.swift | 26 +++++++ 5 files changed, 147 insertions(+), 13 deletions(-) diff --git a/ElementX/Sources/Mocks/CLLocationManagerMock.swift b/ElementX/Sources/Mocks/CLLocationManagerMock.swift index b86eb1929..db611b44f 100644 --- a/ElementX/Sources/Mocks/CLLocationManagerMock.swift +++ b/ElementX/Sources/Mocks/CLLocationManagerMock.swift @@ -10,11 +10,13 @@ import CoreLocation extension CLLocationManagerMock { struct Configuration { var authorizationStatus: CLAuthorizationStatus = .authorizedAlways + var accuracyAuthorization: CLAccuracyAuthorization = .fullAccuracy } convenience init(_ configuration: Configuration) { self.init() underlyingAuthorizationStatus = configuration.authorizationStatus + underlyingAccuracyAuthorization = configuration.accuracyAuthorization } } diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 06542c37a..08163acce 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2214,6 +2214,11 @@ class CLLocationManagerMock: CLLocationManagerProtocol, @unchecked Sendable { set(value) { underlyingAuthorizationStatus = value } } var underlyingAuthorizationStatus: CLAuthorizationStatus! + var accuracyAuthorization: CLAccuracyAuthorization { + get { return underlyingAccuracyAuthorization } + set(value) { underlyingAccuracyAuthorization = value } + } + var underlyingAccuracyAuthorization: CLAccuracyAuthorization! //MARK: - requestAlwaysAuthorization @@ -2320,6 +2325,76 @@ class CLLocationManagerMock: CLLocationManagerProtocol, @unchecked Sendable { stopUpdatingLocationCallsCount += 1 stopUpdatingLocationClosure?() } + //MARK: - startMonitoringSignificantLocationChanges + + var startMonitoringSignificantLocationChangesUnderlyingCallsCount = 0 + var startMonitoringSignificantLocationChangesCallsCount: Int { + get { + if Thread.isMainThread { + return startMonitoringSignificantLocationChangesUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = startMonitoringSignificantLocationChangesUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + startMonitoringSignificantLocationChangesUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + startMonitoringSignificantLocationChangesUnderlyingCallsCount = newValue + } + } + } + } + var startMonitoringSignificantLocationChangesCalled: Bool { + return startMonitoringSignificantLocationChangesCallsCount > 0 + } + var startMonitoringSignificantLocationChangesClosure: (() -> Void)? + + func startMonitoringSignificantLocationChanges() { + startMonitoringSignificantLocationChangesCallsCount += 1 + startMonitoringSignificantLocationChangesClosure?() + } + //MARK: - stopMonitoringSignificantLocationChanges + + var stopMonitoringSignificantLocationChangesUnderlyingCallsCount = 0 + var stopMonitoringSignificantLocationChangesCallsCount: Int { + get { + if Thread.isMainThread { + return stopMonitoringSignificantLocationChangesUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = stopMonitoringSignificantLocationChangesUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + stopMonitoringSignificantLocationChangesUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + stopMonitoringSignificantLocationChangesUnderlyingCallsCount = newValue + } + } + } + } + var stopMonitoringSignificantLocationChangesCalled: Bool { + return stopMonitoringSignificantLocationChangesCallsCount > 0 + } + var stopMonitoringSignificantLocationChangesClosure: (() -> Void)? + + func stopMonitoringSignificantLocationChanges() { + stopMonitoringSignificantLocationChangesCallsCount += 1 + stopMonitoringSignificantLocationChangesClosure?() + } } class CXProviderMock: CXProviderProtocol, @unchecked Sendable { diff --git a/ElementX/Sources/Services/Location/CLLocationManagerProtocol.swift b/ElementX/Sources/Services/Location/CLLocationManagerProtocol.swift index 1b5265e98..ff80c3cb9 100644 --- a/ElementX/Sources/Services/Location/CLLocationManagerProtocol.swift +++ b/ElementX/Sources/Services/Location/CLLocationManagerProtocol.swift @@ -16,10 +16,13 @@ protocol CLLocationManagerProtocol: AnyObject { var desiredAccuracy: CLLocationAccuracy { get set } var distanceFilter: CLLocationDistance { get set } var authorizationStatus: CLAuthorizationStatus { get } + var accuracyAuthorization: CLAccuracyAuthorization { get } func requestAlwaysAuthorization() func startUpdatingLocation() func stopUpdatingLocation() + func startMonitoringSignificantLocationChanges() + func stopMonitoringSignificantLocationChanges() } extension CLLocationManager: CLLocationManagerProtocol { } diff --git a/ElementX/Sources/Services/Location/LiveLocationManager.swift b/ElementX/Sources/Services/Location/LiveLocationManager.swift index 771a83e69..49f4397a4 100644 --- a/ElementX/Sources/Services/Location/LiveLocationManager.swift +++ b/ElementX/Sources/Services/Location/LiveLocationManager.swift @@ -9,11 +9,20 @@ import Combine import CoreLocation class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationManagerDelegate { + enum LiveState { + case full + case limited + case off + } + private let clientProxy: ClientProxyProtocol private let locationManager: CLLocationManagerProtocol private let appSettings: AppSettings private let authorizationStatusSubject: CurrentValueSubject + var authorizationStatus: CurrentValuePublisher { + authorizationStatusSubject.asCurrentValuePublisher() + } /// Cached joined room proxies keyed by room ID, kept in sync with the active sessions dictionary. private var activeRoomProxies = [String: JoinedRoomProxyProtocol]() @@ -23,9 +32,7 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana private var cancellables = Set() - var authorizationStatus: CurrentValuePublisher { - authorizationStatusSubject.asCurrentValuePublisher() - } + private var liveState = LiveState.off @MainActor init(clientProxy: ClientProxyProtocol, @@ -118,6 +125,19 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana stopAllSessions() } + switch (liveState, manager.accuracyAuthorization) { + // If accuracy authorization changed while updates are active, start and stop to switch update method. + case (.full, .reducedAccuracy), (.limited, .fullAccuracy): + stopUpdatingLocation() + if manager.accuracyAuthorization == .fullAccuracy { + // The system has forced reduced desired accuracy so we need to restore the desired value by the user. + setupMinimumDistance(appSettings.liveLocationMinimumDistanceUpdate) + } + startUpdatingLocation() + default: + break + } + authorizationStatusSubject.send(manager.authorizationStatus) } @@ -201,22 +221,30 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana locationManager.distanceFilter = CLLocationDistance(minimumDistance) } - private var isUpdating = false - private func startUpdatingLocation() { - guard !isUpdating else { return } - isUpdating = true + guard liveState == .off else { return } - MXLog.info("Starting live location updates via delegate") - locationManager.startUpdatingLocation() + if locationManager.accuracyAuthorization == .fullAccuracy { + MXLog.info("Starting live location updates with full accuracy") + liveState = .full + locationManager.startUpdatingLocation() + } else { + MXLog.info("Starting live location updates with significant changes (reduced accuracy)") + liveState = .limited + locationManager.startMonitoringSignificantLocationChanges() + } } private func stopUpdatingLocation() { - guard isUpdating else { return } - isUpdating = false + if liveState == .full { + MXLog.info("Stopping live location updates (full accuracy)") + locationManager.stopUpdatingLocation() + } else if liveState == .limited { + MXLog.info("Stopping live location updates (reduced accuracy)") + locationManager.stopMonitoringSignificantLocationChanges() + } - MXLog.info("Stopping live location updates") - locationManager.stopUpdatingLocation() + liveState = .off } private func sendLocationToActiveRooms(_ coordinate: CLLocationCoordinate2D) async { diff --git a/UnitTests/Sources/LiveLocationManagerTests.swift b/UnitTests/Sources/LiveLocationManagerTests.swift index bc9f34194..9272c83ba 100644 --- a/UnitTests/Sources/LiveLocationManagerTests.swift +++ b/UnitTests/Sources/LiveLocationManagerTests.swift @@ -42,6 +42,8 @@ final class LiveLocationManagerTests { #expect(roomProxy.startLiveLocationShareDurationCalled) #expect(!roomProxy.stopLiveLocationShareCalled) #expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] != nil) + #expect(locationManagerMock.startUpdatingLocationCalled) + #expect(!locationManagerMock.startMonitoringSignificantLocationChangesCalled) } @Test @@ -131,6 +133,10 @@ final class LiveLocationManagerTests { #expect(roomProxy.stopLiveLocationShareCalled) #expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] == nil) + // Setting the timeout date above starts tracking; removing it stops tracking. + #expect(locationManagerMock.startUpdatingLocationCalled) + #expect(locationManagerMock.stopUpdatingLocationCalled) + #expect(!locationManagerMock.stopMonitoringSignificantLocationChangesCalled) } @Test @@ -156,6 +162,26 @@ final class LiveLocationManagerTests { #expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room2:matrix.org"] != nil) } + // MARK: - Reduced accuracy + + @Test + func startLiveLocationInReducedAccuracyMode() async throws { + locationManagerMock.underlyingAccuracyAuthorization = .reducedAccuracy + let roomProxy = makeRoomProxy(roomID: "!room:matrix.org") + clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) } + + let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300)) + try result.get() + + #expect(locationManagerMock.startMonitoringSignificantLocationChangesCalled) + #expect(!locationManagerMock.startUpdatingLocationCalled) + + await manager.stopLiveLocation(roomID: "!room:matrix.org") + + #expect(locationManagerMock.stopMonitoringSignificantLocationChangesCalled) + #expect(!locationManagerMock.stopUpdatingLocationCalled) + } + // MARK: - Private private func makeRoomProxy(roomID: String) -> JoinedRoomProxyMock {