diff --git a/Riot/Assets/en.lproj/InfoPlist.strings b/Riot/Assets/en.lproj/InfoPlist.strings
index a9513a6ed..a77ac433c 100644
--- a/Riot/Assets/en.lproj/InfoPlist.strings
+++ b/Riot/Assets/en.lproj/InfoPlist.strings
@@ -22,3 +22,4 @@
"NSCalendarsUsageDescription" = "See your scheduled meetings in the app.";
"NSFaceIDUsageDescription" = "Face ID is used to access your app.";
"NSLocationWhenInUseUsageDescription" = "When you share your location to people, Element needs access to show them a map.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "When you share your location to people, Element needs access to show them a map.";
diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings
index 345b83020..697d08884 100644
--- a/Riot/Assets/en.lproj/Vector.strings
+++ b/Riot/Assets/en.lproj/Vector.strings
@@ -2137,6 +2137,12 @@ Tap the + to start adding people.";
"location_sharing_settings_toggle_title" = "Enable location sharing";
+"location_sharing_allow_background_location_title" = "Allow access";
+"location_sharing_allow_background_location_message" = "If you’d like to share your Live location, Element needs location access when the app is in the background.
+To enable access, tap Settings> Location and select Always";
+"location_sharing_allow_background_location_validate_action" = "Settings";
+"location_sharing_allow_background_location_cancel_action" = "Not now";
+
// MARK: Live location sharing
"location_sharing_live_share_title" = "Share live location";
diff --git a/Riot/Categories/UIApplication.swift b/Riot/Categories/UIApplication.swift
index 11299853d..d2e27c476 100644
--- a/Riot/Categories/UIApplication.swift
+++ b/Riot/Categories/UIApplication.swift
@@ -37,4 +37,12 @@ extension UIApplication {
return sendAction(#selector(resignFirstResponder), to: nil, from: nil, for: nil)
}
+ /// Open system application settings
+ func vc_openSettings(completion: ((Bool) -> Void)? = nil) {
+ guard let applicationSettingsURL = URL(string: UIApplication.openSettingsURLString) else {
+ completion?(false)
+ return
+ }
+ UIApplication.shared.open(applicationSettingsURL, completionHandler: completion)
+ }
}
diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift
index 6cf4c26c7..4130a3cc1 100644
--- a/Riot/Generated/Strings.swift
+++ b/Riot/Generated/Strings.swift
@@ -2751,6 +2751,22 @@ public class VectorL10n: NSObject {
public static func localContactsAccessNotGranted(_ p1: String) -> String {
return VectorL10n.tr("Vector", "local_contacts_access_not_granted", p1)
}
+ /// Not now
+ public static var locationSharingAllowBackgroundLocationCancelAction: String {
+ return VectorL10n.tr("Vector", "location_sharing_allow_background_location_cancel_action")
+ }
+ /// If you’d like to share your Live location, Element needs location access when the app is in the background.\nTo enable access, tap Settings> Location and select Always
+ public static var locationSharingAllowBackgroundLocationMessage: String {
+ return VectorL10n.tr("Vector", "location_sharing_allow_background_location_message")
+ }
+ /// Allow access
+ public static var locationSharingAllowBackgroundLocationTitle: String {
+ return VectorL10n.tr("Vector", "location_sharing_allow_background_location_title")
+ }
+ /// Settings
+ public static var locationSharingAllowBackgroundLocationValidateAction: String {
+ return VectorL10n.tr("Vector", "location_sharing_allow_background_location_validate_action")
+ }
/// Close
public static var locationSharingCloseAction: String {
return VectorL10n.tr("Vector", "location_sharing_close_action")
diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift
index e218a1a11..af141ad72 100755
--- a/Riot/Modules/Application/AppCoordinator.swift
+++ b/Riot/Modules/Application/AppCoordinator.swift
@@ -87,6 +87,9 @@ final class AppCoordinator: NSObject, AppCoordinatorType {
// Setup navigation router store
_ = NavigationRouterStore.shared
+ // Setup user location services
+ _ = UserLocationServiceProvider.shared
+
if BuildSettings.enableSideMenu {
self.addSideMenu()
}
diff --git a/Riot/Modules/LocationSharing/LocationManager.swift b/Riot/Modules/LocationSharing/LocationManager.swift
new file mode 100644
index 000000000..7f8053fa5
--- /dev/null
+++ b/Riot/Modules/LocationSharing/LocationManager.swift
@@ -0,0 +1,216 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import CoreLocation
+
+protocol LocationManagerDelegate: AnyObject {
+ func locationManager(_ manager: LocationManager, didUpdateLocation location: CLLocation)
+}
+
+/// Location accuracy
+enum LocationManagerAccuracy {
+ case full
+ case reduced
+}
+
+/// LocationManager handles device geolocalization
+class LocationManager: NSObject {
+
+ // MARK: - Constants
+
+ private enum Constants {
+ static let distanceFiler: CLLocationDistance = 200.0
+ static let waitForAuthorizationStatusDelay: TimeInterval = 0.5
+ }
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let locationManager: CLLocationManager
+ private var authorizationHandler: LocationAuthorizationHandler?
+
+ // MARK: Public
+
+ class var isLocationEnabled: Bool {
+ return CLLocationManager.locationServicesEnabled()
+ }
+
+ private(set) var accuracy: LocationManagerAccuracy
+
+ var isUpdatingLocation = false
+
+ var lastLocation: CLLocation?
+
+ weak var delegate: LocationManagerDelegate?
+
+ // MARK: - Setup
+
+ init(accuracy: LocationManagerAccuracy, allowsBackgroundLocationUpdates: Bool) {
+
+ self.accuracy = accuracy
+
+ let locationManager = CLLocationManager()
+ locationManager.distanceFilter = Constants.distanceFiler
+
+ let desiredLocationAccuracy: CLLocationAccuracy
+
+ switch accuracy {
+ case .full:
+ desiredLocationAccuracy = kCLLocationAccuracyNearestTenMeters
+ case .reduced:
+ desiredLocationAccuracy = kCLLocationAccuracyHundredMeters
+ }
+
+ locationManager.desiredAccuracy = desiredLocationAccuracy
+ locationManager.allowsBackgroundLocationUpdates = allowsBackgroundLocationUpdates
+
+ self.locationManager = locationManager
+
+ super.init()
+ }
+
+ // MARK: - Public
+
+ /// Start monitoring user location
+ func start() {
+
+ self.locationManager.delegate = self
+
+ switch accuracy {
+ case .full:
+ self.locationManager.startUpdatingLocation()
+ case .reduced:
+ // Only listen to significant changes
+ // roughly after 500 meters moves or every 5 minutes minimum
+ // as mentioned in the Apple documentation https://developer.apple.com/documentation/corelocation/cllocationmanager/1423531-startmonitoringsignificantlocati
+ self.locationManager.startMonitoringSignificantLocationChanges()
+ }
+
+ self.isUpdatingLocation = true
+ }
+
+ /// Stop monitoring user location
+ func stop() {
+
+ switch accuracy {
+ case .full:
+ self.locationManager.startUpdatingLocation()
+ case .reduced:
+ self.locationManager.stopMonitoringSignificantLocationChanges()
+ }
+
+ self.locationManager.delegate = nil
+ self.isUpdatingLocation = false
+ }
+
+ /// Request location authorization
+ func requestAuthorization(_ handler: @escaping LocationAuthorizationHandler) {
+
+ let status = self.locationManager.authorizationStatus
+
+ switch status {
+ case .notDetermined, .authorizedWhenInUse:
+ // Try to resquest always authorization
+ self.tryToRequestAlwaysAuthorization(handler: handler)
+ default:
+ handler(self.locationAuthorizationStatus(from: status))
+ }
+ }
+
+ // MARK: - Private
+
+ // Try to request always authorization and if `locationManagerDidChangeAuthorization` is not called within `Constants.waitForAuthorizationStatusDelay` call the input handler.
+ // NOTE: As pointed in the Apple doc:
+ // - Core Location limits calls to requestAlwaysAuthorization(). After your app calls this method, further calls have no effect.
+ // - If the user responded to requestWhenInUseAuthorization() with Allow Once, then Core Location ignores further calls to requestAlwaysAuthorization() due to the temporary authorization.
+ // See https://developer.apple.com/documentation/corelocation/cllocationmanager/1620551-requestalwaysauthorization?changes=_6_6
+ private func tryToRequestAlwaysAuthorization(handler: @escaping LocationAuthorizationHandler) {
+ self.authorizationHandler = handler
+ self.locationManager.requestAlwaysAuthorization()
+
+ Timer.scheduledTimer(withTimeInterval: Constants.waitForAuthorizationStatusDelay, repeats: false) { [weak self] _ in
+ guard let self = self else {
+ return
+ }
+
+ self.authorizationRequestDidComplete(with: self.locationManager.authorizationStatus)
+ }
+ }
+
+ private func locationAuthorizationStatus(from clLocationAuthorizationStatus: CLAuthorizationStatus) -> LocationAuthorizationStatus {
+
+ let status: LocationAuthorizationStatus
+
+ switch clLocationAuthorizationStatus {
+ case .notDetermined:
+ status = .unknown
+ case .restricted, .denied:
+ status = .denied
+ case .authorizedAlways:
+ status = .authorizedAlways
+ case .authorizedWhenInUse:
+ status = .authorizedInForeground
+ @unknown default:
+ status = .unknown
+ }
+
+ return status
+ }
+
+ private func authorizationRequestDidComplete(with status: CLAuthorizationStatus) {
+ guard let authorizationHandler = self.authorizationHandler else {
+ return
+ }
+
+ authorizationHandler(self.locationAuthorizationStatus(from: status))
+ self.authorizationHandler = nil
+ }
+}
+
+// MARK: - CLLocationManagerDelegate
+extension LocationManager: CLLocationManagerDelegate {
+
+ func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
+
+ let status = self.locationManager.authorizationStatus
+ self.authorizationRequestDidComplete(with: status)
+ }
+
+ func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
+
+ guard let lastLocation = locations.last else {
+ return
+ }
+
+ self.lastLocation = lastLocation
+
+ self.delegate?.locationManager(self, didUpdateLocation: lastLocation)
+ }
+
+ func locationManagerDidResumeLocationUpdates(_ manager: CLLocationManager) {
+ MXLog.debug("[LocationManager] Did resume location updates")
+ }
+
+ func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) {
+ MXLog.debug("[LocationManager] Did pause location updates")
+ }
+
+ func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
+ MXLog.error("[LocationManager] Did failed with error :\(error)")
+ }
+}
diff --git a/Riot/Modules/LocationSharing/MXSession+LocationSharing.swift b/Riot/Modules/LocationSharing/MXSession+LocationSharing.swift
new file mode 100644
index 000000000..e792f12da
--- /dev/null
+++ b/Riot/Modules/LocationSharing/MXSession+LocationSharing.swift
@@ -0,0 +1,30 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import MatrixSDK
+
+extension MXSession {
+
+ /// Convenient getter to retrieve UserLocationService associated to session user id
+ var userLocationService: UserLocationServiceProtocol? {
+ guard let myUserId = self.myUserId else {
+ return nil
+ }
+
+ return UserLocationServiceProvider.shared.locationService(for: myUserId)
+ }
+}
diff --git a/Riot/Modules/LocationSharing/UserLocationService.swift b/Riot/Modules/LocationSharing/UserLocationService.swift
new file mode 100644
index 000000000..69e9b8a20
--- /dev/null
+++ b/Riot/Modules/LocationSharing/UserLocationService.swift
@@ -0,0 +1,258 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+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: false)
+ self.session = session
+ }
+
+ // MARK: - Public
+
+ func requestAuthorization(_ handler: @escaping LocationAuthorizationHandler) {
+
+ self.locationManager.requestAuthorization(handler)
+ }
+
+ func start() {
+ self.startLocationTracking()
+ 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: false) { [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 startListeningBeaconInfoSummaries() {
+ let beaconInfoSummaryListener = self.session.aggregations.beaconAggregations.listenToBeaconInfoSummaryUpdate { roomId, beaconInfoSummary in
+
+ if self.isDeviceBeaconInfoSummary(beaconInfoSummary) {
+
+ 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()
+ }
+ }
+
+ 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 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 with error \(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 startLocationTracking() {
+ self.locationManager.start()
+ self.locationManager.delegate = self
+ }
+
+ 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)
+ }
+}
diff --git a/Riot/Modules/LocationSharing/UserLocationServiceProtocol.swift b/Riot/Modules/LocationSharing/UserLocationServiceProtocol.swift
new file mode 100644
index 000000000..27309a9e8
--- /dev/null
+++ b/Riot/Modules/LocationSharing/UserLocationServiceProtocol.swift
@@ -0,0 +1,30 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+/// Describes service that monitor and share current user device location to rooms where user shared is location
+protocol UserLocationServiceProtocol {
+
+ /// Request location permissions that enables live location sharing
+ func requestAuthorization(_ handler: @escaping LocationAuthorizationHandler)
+
+ /// Start monitoring user location and look to rooms where location should be sent
+ func start()
+
+ /// Stop monitoring user location
+ func stop()
+}
diff --git a/Riot/Modules/LocationSharing/UserLocationServiceProvider.swift b/Riot/Modules/LocationSharing/UserLocationServiceProvider.swift
new file mode 100644
index 000000000..007e745e2
--- /dev/null
+++ b/Riot/Modules/LocationSharing/UserLocationServiceProvider.swift
@@ -0,0 +1,140 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+/// UserLocationServiceProvider enables to automatically store UserLocationService per user id and retrieve existing UserLocationService.
+/// Note: UserLocationService management is not set inside UserSessionsService at the moment to avoid to expose user location management to extension targets.
+class UserLocationServiceProvider {
+
+ // MARK: - Constants
+
+ static let shared = UserLocationServiceProvider()
+
+ // MARK: - Properties
+
+ private var locationServices: [String: UserLocationServiceProtocol] = [:]
+
+ // MARK: - Setup
+
+ private init() {
+ guard BuildSettings.liveLocationSharingEnabled else {
+ return
+ }
+
+ self.registerUserSessionsServiceNotifications()
+ }
+
+ // MARK: - Public
+
+ func locationService(for userId: String) -> UserLocationServiceProtocol? {
+ return self.locationServices[userId]
+ }
+
+ // MARK: - Private
+
+ // MARK: Store
+
+ private func addLocationService(_ userLocationService: UserLocationServiceProtocol, for userId: String) {
+ self.locationServices[userId] = userLocationService
+ }
+
+ private func removeLocationService(for userId: String) {
+ self.locationServices[userId] = nil
+ }
+
+ // MARK: UserLocationService setup
+
+ private func setupUserLocationService(for userSession: UserSession) {
+
+ self.tearDownUserLocationService(for: userSession.userId)
+
+ let userLocationService = UserLocationService(session: userSession.matrixSession)
+
+ self.addLocationService(userLocationService, for: userSession.userId)
+
+ userLocationService.start()
+
+ MXLog.debug("Start monitoring user live location sharing")
+ }
+
+ func setupUserLocationServiceIfNeeded(for userSession: UserSession) {
+
+ // Be sure Matrix session has is store setup to access beacon info summaries
+ guard userSession.matrixSession.state.rawValue >= MXSessionState.storeDataReady.rawValue else {
+ return
+ }
+
+ let locationService = self.locationService(for: userSession.userId)
+
+ guard locationService == nil else {
+ return
+ }
+
+ self.setupUserLocationService(for: userSession)
+ }
+
+ private func tearDownUserLocationService(for userId: String) {
+
+ guard let locationService = self.locationService(for: userId) else {
+ return
+ }
+
+ locationService.stop()
+
+ self.removeLocationService(for: userId)
+
+ MXLog.debug("Stop monitoring user live location sharing")
+ }
+
+ // MARK: UserSessions management
+
+ private func registerUserSessionsServiceNotifications() {
+
+ NotificationCenter.default.addObserver(self, selector: #selector(userSessionsServiceDidAddUserSession(_:)), name: UserSessionsService.didAddUserSession, object: nil)
+
+ NotificationCenter.default.addObserver(self, selector: #selector(userSessionsServiceDidUpdateUserSession(_:)), name: UserSessionsService.userSessionDidChange, object: nil)
+
+ NotificationCenter.default.addObserver(self, selector: #selector(userSessionsServiceDidRemoveUserSession(_:)), name: UserSessionsService.didRemoveUserSession, object: nil)
+ }
+
+ @objc private func userSessionsServiceDidAddUserSession(_ notification: Notification) {
+
+ guard let userInfo = notification.userInfo, let userSession = userInfo[UserSessionsService.NotificationUserInfoKey.userSession] as? UserSession else {
+ return
+ }
+
+ self.setupUserLocationServiceIfNeeded(for: userSession)
+ }
+
+ @objc private func userSessionsServiceDidUpdateUserSession(_ notification: Notification) {
+
+ guard let userInfo = notification.userInfo, let userSession = userInfo[UserSessionsService.NotificationUserInfoKey.userSession] as? UserSession else {
+ return
+ }
+
+ self.setupUserLocationServiceIfNeeded(for: userSession)
+ }
+
+ @objc private func userSessionsServiceDidRemoveUserSession(_ notification: Notification) {
+
+ guard let userInfo = notification.userInfo, let userId = userInfo[UserSessionsService.NotificationUserInfoKey.userId] as? String else {
+ return
+ }
+
+ self.tearDownUserLocationService(for: userId)
+ }
+}
diff --git a/Riot/SupportingFiles/Info.plist b/Riot/SupportingFiles/Info.plist
index e80dcf500..c72fd5609 100644
--- a/Riot/SupportingFiles/Info.plist
+++ b/Riot/SupportingFiles/Info.plist
@@ -67,6 +67,8 @@
Siri is used to perform calls even from the lock screen.
NSLocationWhenInUseUsageDescription
When you share your location to people, Element needs access to show them a map.
+ NSLocationAlwaysAndWhenInUseUsageDescription
+ When you share your location to people, Element needs access to show them a map.
UIBackgroundModes
audio
diff --git a/RiotSwiftUI/Info.plist b/RiotSwiftUI/Info.plist
index 859c68836..a74d21864 100644
--- a/RiotSwiftUI/Info.plist
+++ b/RiotSwiftUI/Info.plist
@@ -24,5 +24,7 @@
RiotSwiftUI
NSLocationWhenInUseUsageDescription
When you share your location to people, Element needs access to show them a map.
+ NSLocationAlwaysAndWhenInUseUsageDescription
+ When you share your location to people, Element needs access to show them a map.
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift
index e89a6f4ed..1abb36aa8 100644
--- a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift
@@ -77,9 +77,12 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
init(parameters: LocationSharingCoordinatorParameters) {
self.parameters = parameters
+
+ let locationSharingService = LocationSharingService(userLocationService: parameters.roomDataSource.mxSession.userLocationService)
+
let viewModel = LocationSharingViewModel(mapStyleURL: BuildSettings.tileServerMapStyleURL,
avatarData: parameters.avatarData,
- isLiveLocationSharingEnabled: BuildSettings.liveLocationSharingEnabled)
+ isLiveLocationSharingEnabled: BuildSettings.liveLocationSharingEnabled, service: locationSharingService)
let view = LocationSharingView(context: viewModel.context)
.addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift
index 9004c0d9b..36aaf6880 100644
--- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift
@@ -28,10 +28,12 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable {
var screenView: ([Any], AnyView) {
+ let locationSharingService = MockLocationSharingService()
+
let mapStyleURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")!
let viewModel = LocationSharingViewModel(mapStyleURL: mapStyleURL,
avatarData: AvatarInput(mxContentUri: "", matrixItemId: "alice:matrix.org", displayName: "Alice"),
- isLiveLocationSharingEnabled: true)
+ isLiveLocationSharingEnabled: true, service: locationSharingService)
return ([viewModel],
AnyView(LocationSharingView(context: viewModel.context)
.addDependency(MockAvatarService.example)))
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift
index e8429ec34..78a38040b 100644
--- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift
@@ -29,13 +29,18 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie
// MARK: Private
+ private let locationSharingService: LocationSharingServiceProtocol
+
// MARK: Public
var completion: ((LocationSharingViewModelResult) -> Void)?
// MARK: - Setup
- init(mapStyleURL: URL, avatarData: AvatarInputProtocol, isLiveLocationSharingEnabled: Bool = false) {
+ init(mapStyleURL: URL, avatarData: AvatarInputProtocol, isLiveLocationSharingEnabled: Bool = false, service: LocationSharingServiceProtocol) {
+
+ self.locationSharingService = service
+
let viewState = LocationSharingViewState(mapStyleURL: mapStyleURL,
userAvatarData: avatarData,
annotations: [],
@@ -75,7 +80,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie
case .goToUserLocation:
state.bindings.pinLocation = nil
case .startLiveSharing:
- state.bindings.showingTimerSelector = true
+ self.startLiveLocationSharing()
case .shareLiveLocation(let timeout):
state.bindings.showingTimerSelector = false
completion?(.shareLiveLocation(timeout: timeout.rawValue))
@@ -124,12 +129,38 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie
title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName),
primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, primaryButtonCompletion),
secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, {
- if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) {
- UIApplication.shared.open(applicationSettingsURL)
- }
+ UIApplication.shared.vc_openSettings()
}))
default:
break
}
}
+
+ private func startLiveLocationSharing() {
+
+ self.locationSharingService.requestAuthorization { [weak self] authorizationStatus in
+
+ guard let self = self else {
+ return
+ }
+
+ switch authorizationStatus {
+ case .unknown, .denied:
+ // Show error alert
+ self.state.bindings.alertInfo = AlertInfo(id: .userLocatingError,
+ title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName),
+ primaryButton: (VectorL10n.ok, { UIApplication.shared.vc_openSettings()
+ }))
+ case .authorizedInForeground:
+ // When user only authorized location in foreground, advize to use background location
+ self.state.bindings.alertInfo = AlertInfo(id: .userLocatingError,
+ title: VectorL10n.locationSharingAllowBackgroundLocationTitle,
+ message: VectorL10n.locationSharingAllowBackgroundLocationMessage,
+ primaryButton: (VectorL10n.locationSharingAllowBackgroundLocationCancelAction, { [weak self] in self?.state.bindings.showingTimerSelector = true }),
+ secondaryButton: (VectorL10n.locationSharingAllowBackgroundLocationValidateAction, { UIApplication.shared.vc_openSettings() }))
+ case .authorizedAlways:
+ self.state.bindings.showingTimerSelector = true
+ }
+ }
+ }
}
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Service/LocationAuthorizationStatus.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Service/LocationAuthorizationStatus.swift
new file mode 100644
index 000000000..ea9b07eaa
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/Service/LocationAuthorizationStatus.swift
@@ -0,0 +1,33 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+/// Location authorization status
+enum LocationAuthorizationStatus {
+
+ /// Location status unknown
+ case unknown
+
+ /// Location access is denied
+ case denied
+
+ /// Location only authorized in foreground
+ case authorizedInForeground
+
+ /// Location only authorized in foreground and background
+ case authorizedAlways
+}
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Service/LocationSharingServiceProtocol.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Service/LocationSharingServiceProtocol.swift
new file mode 100644
index 000000000..64cfac575
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/Service/LocationSharingServiceProtocol.swift
@@ -0,0 +1,28 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import Combine
+import CoreLocation
+
+/// Location authorization request handler
+typealias LocationAuthorizationHandler = (_ authorizationStatus: LocationAuthorizationStatus) -> Void
+
+protocol LocationSharingServiceProtocol {
+
+ /// Request location authorization
+ func requestAuthorization(_ handler: @escaping LocationAuthorizationHandler)
+}
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Service/MatrixSDK/LocationSharingService.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Service/MatrixSDK/LocationSharingService.swift
new file mode 100644
index 000000000..6d40d2ed9
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/Service/MatrixSDK/LocationSharingService.swift
@@ -0,0 +1,47 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import CoreLocation
+import MatrixSDK
+
+class LocationSharingService: LocationSharingServiceProtocol {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let userLocationService: UserLocationServiceProtocol?
+
+ // MARK: Public
+
+ // MARK: - Setup
+
+ init(userLocationService: UserLocationServiceProtocol?) {
+ self.userLocationService = userLocationService
+ }
+
+ // MARK: - Public
+
+ func requestAuthorization(_ handler: @escaping LocationAuthorizationHandler) {
+ guard let userLocationService = self.userLocationService else {
+ handler(LocationAuthorizationStatus.unknown)
+ return
+ }
+
+ userLocationService.requestAuthorization(handler)
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Service/Mock/MockLocationSharingService.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Service/Mock/MockLocationSharingService.swift
new file mode 100644
index 000000000..44eb703fb
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/Service/Mock/MockLocationSharingService.swift
@@ -0,0 +1,26 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import Combine
+import CoreLocation
+
+@available(iOS 14.0, *)
+class MockLocationSharingService: LocationSharingServiceProtocol {
+ func requestAuthorization(_ handler: @escaping LocationAuthorizationHandler) {
+ handler(.authorizedAlways)
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift
index cd871170d..e70a1a2a8 100644
--- a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift
@@ -49,7 +49,7 @@ class LocationSharingViewModelTests: XCTestCase {
XCTFail()
case .cancel:
expectation.fulfill()
- case .shareLiveLocation(timeout: let timeout):
+ case .shareLiveLocation:
XCTFail()
}
}
@@ -94,7 +94,10 @@ class LocationSharingViewModelTests: XCTestCase {
}
private func buildViewModel() -> LocationSharingViewModel {
- LocationSharingViewModel(mapStyleURL: URL(string: "http://empty.com")!,
- avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""))
+
+ let service = MockLocationSharingService()
+
+ return LocationSharingViewModel(mapStyleURL: URL(string: "http://empty.com")!,
+ avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""), service: service)
}
}
diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml
index f2feb1940..2fc955cb9 100644
--- a/RiotSwiftUI/target.yml
+++ b/RiotSwiftUI/target.yml
@@ -53,6 +53,7 @@ targets:
- path: ../Riot/Categories/UIColor.swift
- path: ../Riot/Categories/UISearchBar.swift
- path: ../Riot/Categories/UIView.swift
+ - path: ../Riot/Categories/UIApplication.swift
- path: ../Riot/Assets/en.lproj/Vector.strings
- path: ../Riot/Modules/Analytics/AnalyticsScreen.swift
- path: ../Riot/Assets/en.lproj/Untranslated.strings
diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml
index b7a0f9d00..6bf4196a4 100644
--- a/RiotSwiftUI/targetUITests.yml
+++ b/RiotSwiftUI/targetUITests.yml
@@ -62,6 +62,7 @@ targets:
- path: ../Riot/Categories/UIColor.swift
- path: ../Riot/Categories/UISearchBar.swift
- path: ../Riot/Categories/UIView.swift
+ - path: ../Riot/Categories/UIApplication.swift
- path: ../Riot/Assets/en.lproj/Vector.strings
- path: ../Riot/Modules/Analytics/AnalyticsScreen.swift
- path: ../Riot/Assets/en.lproj/Untranslated.strings
diff --git a/changelog.d/5722.wip b/changelog.d/5722.wip
new file mode 100644
index 000000000..a019ef025
--- /dev/null
+++ b/changelog.d/5722.wip
@@ -0,0 +1 @@
+Location sharing: Support sending live device location.