mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +00:00
use CLLocationManager directly to handle location updates since is more reliable in bg and more configurable
This commit is contained in:
@@ -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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user