use CLLocationManager directly to handle location updates since is more reliable in bg and more configurable

This commit is contained in:
Mauro Romito
2026-04-20 21:48:37 +02:00
committed by Mauro
parent 676c90c484
commit 53f6c66a8b
3 changed files with 129 additions and 46 deletions
@@ -2194,6 +2194,11 @@ class CLLocationManagerMock: CLLocationManagerProtocol, @unchecked Sendable {
set(value) { underlyingAllowsBackgroundLocationUpdates = value } set(value) { underlyingAllowsBackgroundLocationUpdates = value }
} }
var underlyingAllowsBackgroundLocationUpdates: Bool! var underlyingAllowsBackgroundLocationUpdates: Bool!
var showsBackgroundLocationIndicator: Bool {
get { return underlyingShowsBackgroundLocationIndicator }
set(value) { underlyingShowsBackgroundLocationIndicator = value }
}
var underlyingShowsBackgroundLocationIndicator: Bool!
var desiredAccuracy: CLLocationAccuracy { var desiredAccuracy: CLLocationAccuracy {
get { return underlyingDesiredAccuracy } get { return underlyingDesiredAccuracy }
set(value) { underlyingDesiredAccuracy = value } set(value) { underlyingDesiredAccuracy = value }
@@ -2204,11 +2209,6 @@ class CLLocationManagerMock: CLLocationManagerProtocol, @unchecked Sendable {
set(value) { underlyingDistanceFilter = value } set(value) { underlyingDistanceFilter = value }
} }
var underlyingDistanceFilter: CLLocationDistance! var underlyingDistanceFilter: CLLocationDistance!
var pausesLocationUpdatesAutomatically: Bool {
get { return underlyingPausesLocationUpdatesAutomatically }
set(value) { underlyingPausesLocationUpdatesAutomatically = value }
}
var underlyingPausesLocationUpdatesAutomatically: Bool!
var authorizationStatus: CLAuthorizationStatus { var authorizationStatus: CLAuthorizationStatus {
get { return underlyingAuthorizationStatus } get { return underlyingAuthorizationStatus }
set(value) { underlyingAuthorizationStatus = value } set(value) { underlyingAuthorizationStatus = value }
@@ -2250,6 +2250,76 @@ class CLLocationManagerMock: CLLocationManagerProtocol, @unchecked Sendable {
requestAlwaysAuthorizationCallsCount += 1 requestAlwaysAuthorizationCallsCount += 1
requestAlwaysAuthorizationClosure?() requestAlwaysAuthorizationClosure?()
} }
//MARK: - startUpdatingLocation
var startUpdatingLocationUnderlyingCallsCount = 0
var startUpdatingLocationCallsCount: Int {
get {
if Thread.isMainThread {
return startUpdatingLocationUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = startUpdatingLocationUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
startUpdatingLocationUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
startUpdatingLocationUnderlyingCallsCount = newValue
}
}
}
}
var startUpdatingLocationCalled: Bool {
return startUpdatingLocationCallsCount > 0
}
var startUpdatingLocationClosure: (() -> Void)?
func startUpdatingLocation() {
startUpdatingLocationCallsCount += 1
startUpdatingLocationClosure?()
}
//MARK: - stopUpdatingLocation
var stopUpdatingLocationUnderlyingCallsCount = 0
var stopUpdatingLocationCallsCount: Int {
get {
if Thread.isMainThread {
return stopUpdatingLocationUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = stopUpdatingLocationUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
stopUpdatingLocationUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
stopUpdatingLocationUnderlyingCallsCount = newValue
}
}
}
}
var stopUpdatingLocationCalled: Bool {
return stopUpdatingLocationCallsCount > 0
}
var stopUpdatingLocationClosure: (() -> Void)?
func stopUpdatingLocation() {
stopUpdatingLocationCallsCount += 1
stopUpdatingLocationClosure?()
}
} }
class CXProviderMock: CXProviderProtocol, @unchecked Sendable { class CXProviderMock: CXProviderProtocol, @unchecked Sendable {
@@ -7,16 +7,19 @@
import CoreLocation import CoreLocation
/// Protocol for CLLocationManager used for authorization handling and location updates.
// sourcery: AutoMockable // sourcery: AutoMockable
protocol CLLocationManagerProtocol: AnyObject { protocol CLLocationManagerProtocol: AnyObject {
var delegate: CLLocationManagerDelegate? { get set } var delegate: CLLocationManagerDelegate? { get set }
var allowsBackgroundLocationUpdates: Bool { get set } var allowsBackgroundLocationUpdates: Bool { get set }
var showsBackgroundLocationIndicator: Bool { get set }
var desiredAccuracy: CLLocationAccuracy { get set } var desiredAccuracy: CLLocationAccuracy { get set }
var distanceFilter: CLLocationDistance { get set } var distanceFilter: CLLocationDistance { get set }
var pausesLocationUpdatesAutomatically: Bool { get set }
var authorizationStatus: CLAuthorizationStatus { get } var authorizationStatus: CLAuthorizationStatus { get }
func requestAlwaysAuthorization() func requestAlwaysAuthorization()
func startUpdatingLocation()
func stopUpdatingLocation()
} }
extension CLLocationManager: CLLocationManagerProtocol { } extension CLLocationManager: CLLocationManagerProtocol { }
@@ -18,12 +18,8 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
/// Cached joined room proxies keyed by room ID, kept in sync with the active sessions dictionary. /// Cached joined room proxies keyed by room ID, kept in sync with the active sessions dictionary.
private var activeRoomProxies = [String: JoinedRoomProxyProtocol]() private var activeRoomProxies = [String: JoinedRoomProxyProtocol]()
/// The running task that iterates over live location updates.
@CancellableTask
private var locationUpdatesTask: Task<Void, Never>?
/// Subject used to pipe location updates through Combine's throttle operator. /// Subject used to pipe location updates through Combine's throttle operator.
private let locationUpdateSubject = PassthroughSubject<CLLocationUpdate, Never>() private let locationUpdateSubject = PassthroughSubject<CLLocationCoordinate2D, Never>()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
@@ -45,10 +41,11 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
super.init() super.init()
// Configure CLLocationManager for continuous background tracking.
self.locationManager.delegate = self self.locationManager.delegate = self
self.locationManager.allowsBackgroundLocationUpdates = true self.locationManager.allowsBackgroundLocationUpdates = true
self.locationManager.pausesLocationUpdatesAutomatically = false self.locationManager.showsBackgroundLocationIndicator = true
setupMinumDistance(appSettings.liveLocationMinimumDistanceUpdate) setupMinimumDistance(appSettings.liveLocationMinimumDistanceUpdate)
setupSubscriptions() setupSubscriptions()
} }
@@ -124,6 +121,18 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
authorizationStatusSubject.send(manager.authorizationStatus) authorizationStatusSubject.send(manager.authorizationStatus)
} }
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
MXLog.verbose("Received location update via delegate, sending to rooms")
locationUpdateSubject.send(location.coordinate)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
MXLog.error("Location manager failed with error: \(error)")
stopAllSessions()
}
// MARK: - Private // MARK: - Private
private func setupSubscriptions() { private func setupSubscriptions() {
@@ -145,9 +154,9 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
syncActiveRoomProxies(with: sessions) syncActiveRoomProxies(with: sessions)
if sessions.isEmpty { if sessions.isEmpty {
locationUpdatesTask = nil self.stopUpdatingLocation()
} else { } else {
startLocationUpdatesIfNeeded() self.startUpdatingLocation()
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -158,7 +167,7 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
guard let self else { return } guard let self else { return }
appSettings.liveLocationSharingTimeoutDatesByRoomID.removeAll() appSettings.liveLocationSharingTimeoutDatesByRoomID.removeAll()
activeRoomProxies.removeAll() activeRoomProxies.removeAll()
locationUpdatesTask = nil self.stopUpdatingLocation()
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -166,13 +175,21 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
.removeDuplicates() .removeDuplicates()
.debounce(for: .seconds(3), scheduler: DispatchQueue.main) .debounce(for: .seconds(3), scheduler: DispatchQueue.main)
.sink { [weak self] minimumDistance in .sink { [weak self] minimumDistance in
self?.setupMinumDistance(minimumDistance) self?.setupMinimumDistance(minimumDistance)
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
/// Sets up the distance filter and the most optimal accuracy given the minimum distance to save battery, private func syncActiveRoomProxies(with sessions: [String: Date]) {
private func setupMinumDistance(_ minimumDistance: Int) { // Remove proxies for rooms no longer in the dictionary.
let activeRoomIDs = Set(sessions.keys)
for roomID in activeRoomProxies.keys where !activeRoomIDs.contains(roomID) {
activeRoomProxies.removeValue(forKey: roomID)
}
}
/// Sets up the distance filter and the most optimal accuracy given the minimum distance to save battery.
private func setupMinimumDistance(_ minimumDistance: Int) {
switch minimumDistance { switch minimumDistance {
case 0..<10: case 0..<10:
locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.desiredAccuracy = kCLLocationAccuracyBest
@@ -184,34 +201,27 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
locationManager.distanceFilter = CLLocationDistance(minimumDistance) locationManager.distanceFilter = CLLocationDistance(minimumDistance)
} }
private func syncActiveRoomProxies(with sessions: [String: Date]) { private var isUpdating = false
// Remove proxies for rooms no longer in the dictionary.
let activeRoomIDs = Set(sessions.keys)
for roomID in activeRoomProxies.keys where !activeRoomIDs.contains(roomID) {
activeRoomProxies.removeValue(forKey: roomID)
}
}
private func startLocationUpdatesIfNeeded() { private func startUpdatingLocation() {
guard locationUpdatesTask == nil else { return } guard !isUpdating else { return }
isUpdating = true
locationUpdatesTask = Task { [weak self] in MXLog.info("Starting live location updates via delegate")
do { locationManager.startUpdatingLocation()
for try await update in CLLocationUpdate.liveUpdates() {
guard let self, !Task.isCancelled else { break }
self.locationUpdateSubject.send(update)
}
} catch {
MXLog.error("Live location updates failed with error: \(error)")
self?.stopAllSessions()
}
}
} }
private func sendLocationToActiveRooms(_ update: CLLocationUpdate) async { private func stopUpdatingLocation() {
guard isUpdating else { return }
isUpdating = false
MXLog.info("Stopping live location updates")
locationManager.stopUpdatingLocation()
}
private func sendLocationToActiveRooms(_ coordinate: CLLocationCoordinate2D) async {
let sessions = appSettings.liveLocationSharingTimeoutDatesByRoomID let sessions = appSettings.liveLocationSharingTimeoutDatesByRoomID
let geoURI = update.location.map { GeoURI(coordinate: $0.coordinate, uncertainty: $0.horizontalAccuracy) } let geoURI = GeoURI(coordinate: coordinate, uncertainty: nil)
for (roomID, timeoutDate) in sessions { for (roomID, timeoutDate) in sessions {
if Date() >= timeoutDate { if Date() >= timeoutDate {
@@ -220,16 +230,16 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
continue continue
} }
guard let geoURI else { continue }
let roomProxy = await resolveRoomProxy(for: roomID) let roomProxy = await resolveRoomProxy(for: roomID)
guard let roomProxy else { guard let roomProxy else {
MXLog.error("Failed to resolve room proxy for live location update in room: \(roomID)") MXLog.error("Failed to resolve room proxy for live location update in room: \(roomID)")
continue continue
} }
let result = await roomProxy.sendLiveLocation(geoURI: geoURI) switch await roomProxy.sendLiveLocation(geoURI: geoURI) {
if case .failure(let error) = result { case .success:
MXLog.debug("Sent live location to room: \(roomID)")
case .failure(let error):
MXLog.error("Failed to send live location update to room \(roomID): \(error)") MXLog.error("Failed to send live location update to room \(roomID): \(error)")
} }
} }