Live Location Sharing - handle server echoes (#5514)

* Track active live location sessions by ID instead of timeout.

# Conflicts:
#	ElementX/Sources/Services/Location/LiveLocationManager.swift

* implemented a system to promote starting session to active sesessions to send locations at the right time, and a system to remove a local session if it's handled by an external device.

* pr suggestions

---------

Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
Mauro
2026-04-30 15:18:36 +02:00
committed by GitHub
parent f310ec9e82
commit 11584d6bfe
13 changed files with 209 additions and 61 deletions

View File

@@ -5,6 +5,7 @@
// Please see LICENSE files in the repository root for full details.
//
import Combine
import CoreLocation
@testable import ElementX
import Foundation
@@ -16,6 +17,7 @@ final class LiveLocationManagerTests {
private var locationManagerMock: CLLocationManagerMock!
private var manager: LiveLocationManager!
private var appSettings: AppSettings!
private var beaconInfoSubject: PassthroughSubject<LiveLocationOwnInfoUpdate, Never>!
init() {
AppSettings.resetAllSettings()
@@ -40,14 +42,18 @@ final class LiveLocationManagerTests {
}
roomProxy.startLiveLocationShareDurationClosure = { _ in
callOrder.append("start")
return .success(())
return .success("$event:matrix.org")
}
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
try result.get()
#expect(callOrder == ["stop", "start"])
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] != nil)
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] == nil)
try await simulateBeaconEcho(roomID: "!room:matrix.org", eventID: "$event:matrix.org")
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] != nil)
#expect(locationManagerMock.startUpdatingLocationCalled)
}
@@ -56,7 +62,7 @@ final class LiveLocationManagerTests {
setUp()
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] = Date().addingTimeInterval(300)
appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] = LiveLocationSession(eventID: "$old_event:matrix.org", expirationDate: Date().addingTimeInterval(300))
var callOrder: [String] = []
roomProxy.stopLiveLocationShareClosure = {
@@ -65,14 +71,17 @@ final class LiveLocationManagerTests {
}
roomProxy.startLiveLocationShareDurationClosure = { _ in
callOrder.append("start")
return .success(())
return .success("$event:matrix.org")
}
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(600))
try result.get()
#expect(callOrder == ["stop", "start"])
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] != nil)
try await simulateBeaconEcho(roomID: "!room:matrix.org", eventID: "$event:matrix.org")
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] != nil)
}
@Test
@@ -81,12 +90,12 @@ final class LiveLocationManagerTests {
let roomProxy = makeRoomProxy(roomID: "!room1:matrix.org")
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room2:matrix.org"] = Date().addingTimeInterval(300)
appSettings.liveLocationSharingSessionsByRoomID["!room2:matrix.org"] = LiveLocationSession(eventID: "$event:matrix.org", expirationDate: Date().addingTimeInterval(300))
_ = await manager.startLiveLocation(roomID: "!room1:matrix.org", duration: .seconds(300))
#expect(roomProxy.stopLiveLocationShareCalled)
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room2:matrix.org"] != nil)
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room2:matrix.org"] != nil)
}
@Test
@@ -97,7 +106,7 @@ final class LiveLocationManagerTests {
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
#expect(throws: LiveLocationManagerError.roomNotJoined) { try result.get() }
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] == nil)
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] == nil)
}
@Test
@@ -110,7 +119,7 @@ final class LiveLocationManagerTests {
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
#expect(throws: LiveLocationManagerError.startFailed) { try result.get() }
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] == nil)
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] == nil)
}
@Test
@@ -124,11 +133,14 @@ final class LiveLocationManagerTests {
_ = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: duration)
let afterStart = Date()
let storedTimeout = appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"]
try await simulateBeaconEcho(roomID: "!room:matrix.org", eventID: "$event:matrix.org")
let storedSession = try #require(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"])
let expectedMinTimeout = beforeStart.addingTimeInterval(TimeInterval(duration.seconds))
let expectedMaxTimeout = afterStart.addingTimeInterval(TimeInterval(duration.seconds))
try #expect((expectedMinTimeout...expectedMaxTimeout).contains(#require(storedTimeout)))
#expect((expectedMinTimeout...expectedMaxTimeout).contains(storedSession.expirationDate))
#expect(storedSession.eventID == "$event:matrix.org")
}
// MARK: - stopLiveLocation
@@ -138,12 +150,12 @@ final class LiveLocationManagerTests {
setUp()
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] = Date().addingTimeInterval(300)
appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] = LiveLocationSession(eventID: "$event:matrix.org", expirationDate: Date().addingTimeInterval(300))
await manager.stopLiveLocation(roomID: "!room:matrix.org")
#expect(roomProxy.stopLiveLocationShareCalled)
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] == nil)
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] == nil)
// Setting the timeout date above starts tracking; removing it stops tracking.
#expect(locationManagerMock.startUpdatingLocationCalled)
#expect(locationManagerMock.stopUpdatingLocationCalled)
@@ -165,15 +177,34 @@ final class LiveLocationManagerTests {
setUp()
let roomProxy = makeRoomProxy(roomID: "!room1:matrix.org")
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room1:matrix.org"] = Date().addingTimeInterval(300)
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room2:matrix.org"] = Date().addingTimeInterval(300)
appSettings.liveLocationSharingSessionsByRoomID["!room1:matrix.org"] = LiveLocationSession(eventID: "$event:matrix.org", expirationDate: Date().addingTimeInterval(300))
appSettings.liveLocationSharingSessionsByRoomID["!room2:matrix.org"] = LiveLocationSession(eventID: "$event:matrix.org", expirationDate: Date().addingTimeInterval(300))
await manager.stopLiveLocation(roomID: "!room1:matrix.org")
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room1:matrix.org"] == nil)
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room2:matrix.org"] != nil)
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room1:matrix.org"] == nil)
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room2:matrix.org"] != nil)
}
// MARK: - Beacon info updates
@Test
func beaconInfoUpdateFromAnotherDeviceRemovesActiveSession() async throws {
setUp()
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
try await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300)).get()
try await simulateBeaconEcho(roomID: "!room:matrix.org", eventID: "$event:matrix.org")
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] != nil)
let deferred = deferFulfillment(appSettings.$liveLocationSharingSessionsByRoomID) { $0["!room:matrix.org"] == nil }
beaconInfoSubject.send(LiveLocationOwnInfoUpdate(roomID: "!room:matrix.org", eventID: "$external_event:matrix.org", isLive: true))
try await deferred.fulfill()
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] == nil)
}
// MARK: - Reduced accuracy
@Test
@@ -185,6 +216,8 @@ final class LiveLocationManagerTests {
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
try result.get()
try await simulateBeaconEcho(roomID: "!room:matrix.org", eventID: "$event:matrix.org")
#expect(locationManagerMock.startUpdatingLocationCalled)
#expect(locationManagerMock.desiredAccuracy == kCLLocationAccuracyReduced)
@@ -197,7 +230,7 @@ final class LiveLocationManagerTests {
private func makeRoomProxy(roomID: String) -> JoinedRoomProxyMock {
let roomProxy = JoinedRoomProxyMock(.init(id: roomID))
roomProxy.startLiveLocationShareDurationReturnValue = .success(())
roomProxy.startLiveLocationShareDurationReturnValue = .success("$event:matrix.org")
roomProxy.stopLiveLocationShareReturnValue = .success(())
return roomProxy
}
@@ -205,7 +238,15 @@ final class LiveLocationManagerTests {
private func setUp(accuracyAuthorization: CLAccuracyAuthorization = .fullAccuracy) {
appSettings = AppSettings()
clientProxy = ClientProxyMock(.init())
beaconInfoSubject = PassthroughSubject<LiveLocationOwnInfoUpdate, Never>()
clientProxy.liveLocationOwnInfoUpdatesPublisher = beaconInfoSubject.eraseToAnyPublisher()
locationManagerMock = CLLocationManagerMock(.init(accuracyAuthorization: accuracyAuthorization))
manager = LiveLocationManager(clientProxy: clientProxy, appSettings: appSettings, locationManager: locationManagerMock)
}
private func simulateBeaconEcho(roomID: String, eventID: String) async throws {
let deferred = deferFulfillment(appSettings.$liveLocationSharingSessionsByRoomID) { $0[roomID] != nil }
beaconInfoSubject.send(LiveLocationOwnInfoUpdate(roomID: roomID, eventID: eventID, isLive: true))
try await deferred.fulfill()
}
}