element-ios/Riot/Modules/LocationSharing/UserLocationService.swift

277 lines
9.4 KiB
Swift

//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
import CoreLocation
import MatrixSDK
/// UserLocationService handles live location sharing for the current user
class UserLocationService: UserLocationServiceProtocol {
// MARK: - Constants
private enum Constants {
/// Minimum delay in milliseconds to send consecutive location for a beacon info
static let beaconSendMinInterval: UInt64 = 5000 // 5s
/// Delay to check for experied beacons
static let beaconExpiredVerificationInterval: TimeInterval = 5 // 5s
}
// MARK: - Properties
// MARK: Private
private let locationManager: LocationManager
private let session: MXSession
/// All active beacon info summaries that belongs to this device
/// Do not update location for beacon info started on another device
private var deviceBeaconInfoSummaries: [MXBeaconInfoSummaryProtocol] = []
private var beaconInfoSummaryListener: Any?
private var expiredBeaconVerificationTimer: Timer?
// MARK: Public
// MARK: - Setup
init(session: MXSession) {
self.locationManager = LocationManager(accuracy: .full, allowsBackgroundLocationUpdates: BuildSettings.locationSharingEnabled)
self.session = session
}
// MARK: - Public
func requestAuthorization(_ handler: @escaping LocationAuthorizationHandler) {
self.locationManager.requestAuthorization(handler)
}
func start() {
self.locationManager.delegate = self
// Check for existing beacon info summaries for the current device and start location tracking if needed
self.setupDeviceBeaconSummaries()
self.startListeningBeaconInfoSummaries()
self.startVerifyingExpiredBeaconInfoSummaries()
}
func stop() {
self.stopLocationTracking()
self.stopListeningBeaconInfoSummaries()
self.stopVerifyingExpiredBeaconInfoSummaries()
}
// MARK: - Private
// MARK: Beacon info summary
private func startVerifyingExpiredBeaconInfoSummaries() {
let timer = Timer.scheduledTimer(withTimeInterval: Constants.beaconExpiredVerificationInterval, repeats: true) { [weak self] _ in
self?.verifyExpiredBeaconInfoSummaries()
}
self.expiredBeaconVerificationTimer = timer
}
private func stopVerifyingExpiredBeaconInfoSummaries() {
self.expiredBeaconVerificationTimer?.invalidate()
self.expiredBeaconVerificationTimer = nil
}
private func verifyExpiredBeaconInfoSummaries() {
for beaconInfoSummary in deviceBeaconInfoSummaries where beaconInfoSummary.isActive == false && beaconInfoSummary.hasStopped == false {
// TODO: Prevent to stop several times
// Wait for isStopping status
self.session.locationService.stopUserLocationSharing(withBeaconInfoEventId: beaconInfoSummary.id, roomId: beaconInfoSummary.roomId) { response in
}
}
// Remove non active beacon info summaries
self.deviceBeaconInfoSummaries = self.deviceBeaconInfoSummaries.filter({ beaconInfoSummary in
return beaconInfoSummary.isActive
})
}
private func getExistingDeviceBeaconSummaries() -> [MXBeaconInfoSummaryProtocol] {
guard let userId = self.session.myUserId else {
return []
}
return self.session.locationService.getBeaconInfoSummaries(for: userId).filter { summary in
return self.isDeviceBeaconInfoSummary(summary) && summary.isActive
}
}
private func setupDeviceBeaconSummaries() {
let existingDeviceBeaconInfoSummaries = self.getExistingDeviceBeaconSummaries()
self.deviceBeaconInfoSummaries = existingDeviceBeaconInfoSummaries
self.updateLocationTrackingIfNeeded()
for summary in existingDeviceBeaconInfoSummaries {
self.didReceiveDeviceNewBeaconInfoSummary(summary)
}
}
private func startListeningBeaconInfoSummaries() {
let beaconInfoSummaryListener = self.session.aggregations.beaconAggregations.listenToBeaconInfoSummaryUpdate { [weak self] roomId, beaconInfoSummary in
self?.didReceiveBeaconInfoSummary(beaconInfoSummary)
}
self.beaconInfoSummaryListener = beaconInfoSummaryListener
}
private func stopListeningBeaconInfoSummaries() {
if let listener = self.beaconInfoSummaryListener {
self.session.aggregations.beaconAggregations.removeListener(listener)
}
}
private func isDeviceBeaconInfoSummary(_ beaconInfoSummary: MXBeaconInfoSummaryProtocol) -> Bool {
return beaconInfoSummary.userId == self.session.myUserId && beaconInfoSummary.deviceId == self.session.myDeviceId
}
private func didReceiveBeaconInfoSummary(_ beaconInfoSummary: MXBeaconInfoSummaryProtocol) {
guard self.isDeviceBeaconInfoSummary(beaconInfoSummary) else {
return
}
let existingIndex = self.deviceBeaconInfoSummaries.firstIndex(where: { beaconInfoSum in
beaconInfoSum.id == beaconInfoSummary.id
})
if beaconInfoSummary.isActive {
if let index = existingIndex {
self.deviceBeaconInfoSummaries[index] = beaconInfoSummary
} else {
self.deviceBeaconInfoSummaries.append(beaconInfoSummary)
// Send location if possible to a new beacon info summary
self.didReceiveDeviceNewBeaconInfoSummary(beaconInfoSummary)
}
} else {
if let index = existingIndex {
self.deviceBeaconInfoSummaries.remove(at: index)
}
}
self.updateLocationTrackingIfNeeded()
}
private func didReceiveDeviceNewBeaconInfoSummary(_ beaconInfoSummary: MXBeaconInfoSummaryProtocol) {
guard let lastLocation = self.locationManager.lastLocation else {
return
}
self.sendLocation(lastLocation, for: beaconInfoSummary)
}
// MARK: Location sending
private func sendLocation(_ location: CLLocation, for beaconInfoSummary: MXBeaconInfoSummaryProtocol) {
guard self.canSendBeaconRequest(for: beaconInfoSummary) else {
return
}
var localEcho: MXEvent?
self.session.locationService.sendLocation(withBeaconInfoEventId: beaconInfoSummary.id,
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude,
inRoomWithId: beaconInfoSummary.roomId,
localEcho: &localEcho) { response in
switch response {
case .success:
break
case .failure(let error):
MXLog.error("Fail to send location", context: error)
}
}
}
private func didReceiveLocation(_ location: CLLocation) {
for deviceBaconInfoSummary in deviceBeaconInfoSummaries {
self.sendLocation(location, for: deviceBaconInfoSummary)
}
}
private func canSendBeaconRequest(for beaconInfoSummary: MXBeaconInfoSummaryProtocol) -> Bool {
// Check if location manager is started
guard self.locationManager.isUpdatingLocation else {
return false
}
let canSendBeaconRequest: Bool
if let lastBeaconTimestamp = beaconInfoSummary.lastBeacon?.timestamp {
let currentTimestamp = Date().timeIntervalSince1970 * 1000
canSendBeaconRequest = UInt64(currentTimestamp) - lastBeaconTimestamp >= Constants.beaconSendMinInterval
} else {
// The beacon info summary have no last beacon, we can send a request immediatly
canSendBeaconRequest = true
}
return canSendBeaconRequest
}
// MARK: Device location
private func stopLocationTracking() {
self.locationManager.stop()
self.locationManager.delegate = nil
}
private func updateLocationTrackingIfNeeded() {
if self.deviceBeaconInfoSummaries.isEmpty {
// Stop location tracking if there is no active beacon info summaries
if self.locationManager.isUpdatingLocation {
self.locationManager.stop()
}
} else {
// Start location tracking if there is beacon info summaries and location tracking is stopped
if self.locationManager.isUpdatingLocation == false {
self.locationManager.start()
}
}
}
}
// MARK: - LocationManagerDelegate
extension UserLocationService: LocationManagerDelegate {
func locationManager(_ manager: LocationManager, didUpdateLocation location: CLLocation) {
self.didReceiveLocation(location)
}
}