Improve Live Location Sharing with reduced accuracy mode (#5461)

This commit is contained in:
Mauro
2026-04-22 13:12:03 +02:00
committed by GitHub
parent d95c5c51a4
commit e9fd29e100
4 changed files with 60 additions and 133 deletions

View File

@@ -2209,6 +2209,11 @@ 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 }
@@ -2325,76 +2330,6 @@ class CLLocationManagerMock: CLLocationManagerProtocol, @unchecked Sendable {
stopUpdatingLocationCallsCount += 1 stopUpdatingLocationCallsCount += 1
stopUpdatingLocationClosure?() 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 { class CXProviderMock: CXProviderProtocol, @unchecked Sendable {

View File

@@ -15,14 +15,13 @@ protocol CLLocationManagerProtocol: AnyObject {
var showsBackgroundLocationIndicator: 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 }
var accuracyAuthorization: CLAccuracyAuthorization { get } var accuracyAuthorization: CLAccuracyAuthorization { get }
func requestAlwaysAuthorization() func requestAlwaysAuthorization()
func startUpdatingLocation() func startUpdatingLocation()
func stopUpdatingLocation() func stopUpdatingLocation()
func startMonitoringSignificantLocationChanges()
func stopMonitoringSignificantLocationChanges()
} }
extension CLLocationManager: CLLocationManagerProtocol { } extension CLLocationManager: CLLocationManagerProtocol { }

View File

@@ -9,12 +9,6 @@ import Combine
import CoreLocation import CoreLocation
class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationManagerDelegate { class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationManagerDelegate {
enum LiveState {
case full
case limited
case off
}
private let clientProxy: ClientProxyProtocol private let clientProxy: ClientProxyProtocol
private let locationManager: CLLocationManagerProtocol private let locationManager: CLLocationManagerProtocol
private let appSettings: AppSettings private let appSettings: AppSettings
@@ -32,7 +26,7 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var liveState = LiveState.off private var isUpdatingLocation = false
@MainActor @MainActor
init(clientProxy: ClientProxyProtocol, init(clientProxy: ClientProxyProtocol,
@@ -52,7 +46,13 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
self.locationManager.delegate = self self.locationManager.delegate = self
self.locationManager.allowsBackgroundLocationUpdates = true self.locationManager.allowsBackgroundLocationUpdates = true
self.locationManager.showsBackgroundLocationIndicator = true self.locationManager.showsBackgroundLocationIndicator = true
setupMinimumDistance(appSettings.liveLocationMinimumDistanceUpdate)
// Since unpausing location updates is not trivial, let's always keep the location updates running
// The distance filtering will already take care of not sending updates when not required.
// https://developer.apple.com/documentation/corelocation/cllocationmanager/pauseslocationupdatesautomatically
self.locationManager.pausesLocationUpdatesAutomatically = false
setupMinimumDistanceUpdatesAndAccuracy(minimumDistance: appSettings.liveLocationMinimumDistanceUpdate)
setupSubscriptions() setupSubscriptions()
} }
@@ -125,18 +125,8 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
stopAllSessions() stopAllSessions()
} }
switch (liveState, manager.accuracyAuthorization) { // Accuracy authorization may have changed, reapply new accuracy settings.
// If accuracy authorization changed while updates are active, start and stop to switch update method. setupMinimumDistanceUpdatesAndAccuracy(minimumDistance: appSettings.liveLocationMinimumDistanceUpdate)
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) authorizationStatusSubject.send(manager.authorizationStatus)
} }
@@ -194,8 +184,8 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
appSettings.$liveLocationMinimumDistanceUpdate appSettings.$liveLocationMinimumDistanceUpdate
.removeDuplicates() .removeDuplicates()
.debounce(for: .seconds(1), scheduler: DispatchQueue.main) .debounce(for: .seconds(1), scheduler: DispatchQueue.main)
.sink { [weak self] minimumDistance in .sink { [weak self] newValue in
self?.setupMinimumDistance(minimumDistance) self?.setupMinimumDistanceUpdatesAndAccuracy(minimumDistance: newValue)
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
@@ -209,42 +199,36 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
} }
/// Sets up the distance filter and the most optimal accuracy given the minimum distance to save battery. /// Sets up the distance filter and the most optimal accuracy given the minimum distance to save battery.
private func setupMinimumDistance(_ minimumDistance: Int) { private func setupMinimumDistanceUpdatesAndAccuracy(minimumDistance: Int) {
switch minimumDistance { if locationManager.accuracyAuthorization == .fullAccuracy {
case 0..<10: switch minimumDistance {
locationManager.desiredAccuracy = kCLLocationAccuracyBest case 0..<10:
case 10..<100: locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters case 10..<100:
default: locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters default:
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
}
} else {
locationManager.desiredAccuracy = kCLLocationAccuracyReduced
} }
locationManager.distanceFilter = CLLocationDistance(minimumDistance) locationManager.distanceFilter = CLLocationDistance(minimumDistance)
} }
private func startUpdatingLocation() { private func startUpdatingLocation() {
guard liveState == .off else { return } guard !isUpdatingLocation else { return }
if locationManager.accuracyAuthorization == .fullAccuracy { MXLog.info("Starting live location updates")
MXLog.info("Starting live location updates with full accuracy") isUpdatingLocation = true
liveState = .full locationManager.startUpdatingLocation()
locationManager.startUpdatingLocation()
} else {
MXLog.info("Starting live location updates with significant changes (reduced accuracy)")
liveState = .limited
locationManager.startMonitoringSignificantLocationChanges()
}
} }
private func stopUpdatingLocation() { private func stopUpdatingLocation() {
if liveState == .full { guard isUpdatingLocation else { return }
MXLog.info("Stopping live location updates (full accuracy)")
locationManager.stopUpdatingLocation()
} else if liveState == .limited {
MXLog.info("Stopping live location updates (reduced accuracy)")
locationManager.stopMonitoringSignificantLocationChanges()
}
liveState = .off MXLog.info("Stopping live location updates")
locationManager.stopUpdatingLocation()
isUpdatingLocation = false
} }
private func sendLocationToActiveRooms(_ coordinate: CLLocationCoordinate2D) async { private func sendLocationToActiveRooms(_ coordinate: CLLocationCoordinate2D) async {

View File

@@ -5,6 +5,7 @@
// Please see LICENSE files in the repository root for full details. // Please see LICENSE files in the repository root for full details.
// //
import CoreLocation
@testable import ElementX @testable import ElementX
import Foundation import Foundation
import Testing import Testing
@@ -14,15 +15,10 @@ final class LiveLocationManagerTests {
private var clientProxy: ClientProxyMock! private var clientProxy: ClientProxyMock!
private var locationManagerMock: CLLocationManagerMock! private var locationManagerMock: CLLocationManagerMock!
private var manager: LiveLocationManager! private var manager: LiveLocationManager!
private var appSettings: AppSettings!
private let appSettings: AppSettings
init() { init() {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
appSettings = AppSettings()
clientProxy = ClientProxyMock(.init())
locationManagerMock = CLLocationManagerMock(.init())
manager = LiveLocationManager(clientProxy: clientProxy, appSettings: appSettings, locationManager: locationManagerMock)
} }
deinit { deinit {
@@ -33,6 +29,7 @@ final class LiveLocationManagerTests {
@Test @Test
func startLiveLocationWithNoExistingSession() async throws { func startLiveLocationWithNoExistingSession() async throws {
setUp()
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org") let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) } clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
@@ -43,11 +40,11 @@ final class LiveLocationManagerTests {
#expect(!roomProxy.stopLiveLocationShareCalled) #expect(!roomProxy.stopLiveLocationShareCalled)
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] != nil) #expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] != nil)
#expect(locationManagerMock.startUpdatingLocationCalled) #expect(locationManagerMock.startUpdatingLocationCalled)
#expect(!locationManagerMock.startMonitoringSignificantLocationChangesCalled)
} }
@Test @Test
func startLiveLocationWithExistingSessionStopsItFirst() async throws { func startLiveLocationWithExistingSessionStopsItFirst() async throws {
setUp()
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org") let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) } clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] = Date().addingTimeInterval(300) appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] = Date().addingTimeInterval(300)
@@ -71,6 +68,7 @@ final class LiveLocationManagerTests {
@Test @Test
func startLiveLocationDoesNotStopSessionForOtherRoom() async { func startLiveLocationDoesNotStopSessionForOtherRoom() async {
setUp()
let roomProxy = makeRoomProxy(roomID: "!room1:matrix.org") let roomProxy = makeRoomProxy(roomID: "!room1:matrix.org")
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) } clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
@@ -84,6 +82,7 @@ final class LiveLocationManagerTests {
@Test @Test
func startLiveLocationWhenRoomNotJoined() async { func startLiveLocationWhenRoomNotJoined() async {
setUp()
clientProxy.roomForIdentifierClosure = { _ in nil } clientProxy.roomForIdentifierClosure = { _ in nil }
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300)) let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
@@ -94,6 +93,7 @@ final class LiveLocationManagerTests {
@Test @Test
func startLiveLocationWhenStartShareFails() async { func startLiveLocationWhenStartShareFails() async {
setUp()
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org") let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
roomProxy.startLiveLocationShareDurationReturnValue = .failure(.sdkError(RoomProxyMockError.generic)) roomProxy.startLiveLocationShareDurationReturnValue = .failure(.sdkError(RoomProxyMockError.generic))
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) } clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
@@ -106,6 +106,7 @@ final class LiveLocationManagerTests {
@Test @Test
func startLiveLocationStoresTimeoutDate() async throws { func startLiveLocationStoresTimeoutDate() async throws {
setUp()
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org") let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) } clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
@@ -125,6 +126,7 @@ final class LiveLocationManagerTests {
@Test @Test
func stopLiveLocationWhenSessionExists() async { func stopLiveLocationWhenSessionExists() async {
setUp()
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org") let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) } clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] = Date().addingTimeInterval(300) appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] = Date().addingTimeInterval(300)
@@ -136,11 +138,11 @@ final class LiveLocationManagerTests {
// Setting the timeout date above starts tracking; removing it stops tracking. // Setting the timeout date above starts tracking; removing it stops tracking.
#expect(locationManagerMock.startUpdatingLocationCalled) #expect(locationManagerMock.startUpdatingLocationCalled)
#expect(locationManagerMock.stopUpdatingLocationCalled) #expect(locationManagerMock.stopUpdatingLocationCalled)
#expect(!locationManagerMock.stopMonitoringSignificantLocationChangesCalled)
} }
@Test @Test
func stopLiveLocationWhenNoSession() async { func stopLiveLocationWhenNoSession() async {
setUp()
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org") let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) } clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
@@ -151,6 +153,7 @@ final class LiveLocationManagerTests {
@Test @Test
func stopLiveLocationDoesNotRemoveOtherSessions() async { func stopLiveLocationDoesNotRemoveOtherSessions() async {
setUp()
let roomProxy = makeRoomProxy(roomID: "!room1:matrix.org") let roomProxy = makeRoomProxy(roomID: "!room1:matrix.org")
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) } clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room1:matrix.org"] = Date().addingTimeInterval(300) appSettings.liveLocationSharingTimeoutDatesByRoomID["!room1:matrix.org"] = Date().addingTimeInterval(300)
@@ -166,20 +169,19 @@ final class LiveLocationManagerTests {
@Test @Test
func startLiveLocationInReducedAccuracyMode() async throws { func startLiveLocationInReducedAccuracyMode() async throws {
locationManagerMock.underlyingAccuracyAuthorization = .reducedAccuracy setUp(accuracyAuthorization: .reducedAccuracy)
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org") let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) } clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300)) let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
try result.get() try result.get()
#expect(locationManagerMock.startMonitoringSignificantLocationChangesCalled) #expect(locationManagerMock.startUpdatingLocationCalled)
#expect(!locationManagerMock.startUpdatingLocationCalled) #expect(locationManagerMock.desiredAccuracy == kCLLocationAccuracyReduced)
await manager.stopLiveLocation(roomID: "!room:matrix.org") await manager.stopLiveLocation(roomID: "!room:matrix.org")
#expect(locationManagerMock.stopMonitoringSignificantLocationChangesCalled) #expect(locationManagerMock.stopUpdatingLocationCalled)
#expect(!locationManagerMock.stopUpdatingLocationCalled)
} }
// MARK: - Private // MARK: - Private
@@ -190,4 +192,11 @@ final class LiveLocationManagerTests {
roomProxy.stopLiveLocationShareReturnValue = .success(()) roomProxy.stopLiveLocationShareReturnValue = .success(())
return roomProxy return roomProxy
} }
private func setUp(accuracyAuthorization: CLAccuracyAuthorization = .fullAccuracy) {
appSettings = AppSettings()
clientProxy = ClientProxyMock(.init())
locationManagerMock = CLLocationManagerMock(.init(accuracyAuthorization: accuracyAuthorization))
manager = LiveLocationManager(clientProxy: clientProxy, appSettings: appSettings, locationManager: locationManagerMock)
}
} }