224 lines
8.3 KiB
Swift
224 lines
8.3 KiB
Swift
// Copyright 2023-present 650 Industries. All rights reserved.
|
|
|
|
import CoreLocation
|
|
import ExpoModulesCore
|
|
|
|
private let EVENT_LOCATION_CHANGED = "Expo.locationChanged"
|
|
private let EVENT_HEADING_CHANGED = "Expo.headingChanged"
|
|
private let EVENT_LOCATION_ERROR = "Expo.locationError"
|
|
|
|
public final class LocationModule: Module {
|
|
private lazy var locationStreamers = [Int: BaseStreamer]()
|
|
|
|
private var taskManager: EXTaskManagerInterface {
|
|
get throws {
|
|
guard let taskManager: EXTaskManagerInterface = appContext?.legacyModule(implementing: EXTaskManagerInterface.self) else {
|
|
throw Exceptions.TaskManagerUnavailable()
|
|
}
|
|
return taskManager
|
|
}
|
|
}
|
|
|
|
public func definition() -> ModuleDefinition {
|
|
Name("ExpoLocation")
|
|
|
|
Events(EVENT_LOCATION_CHANGED, EVENT_HEADING_CHANGED, EVENT_LOCATION_ERROR)
|
|
|
|
OnCreate {
|
|
let permissionsManager = self.appContext?.permissions
|
|
EXPermissionsMethodsDelegate.register(
|
|
[
|
|
EXLocationPermissionRequester(),
|
|
EXForegroundPermissionRequester(),
|
|
EXBackgroundLocationPermissionRequester()
|
|
],
|
|
withPermissionsManager: permissionsManager
|
|
)
|
|
}
|
|
|
|
AsyncFunction("getProviderStatusAsync") {
|
|
return [
|
|
"locationServicesEnabled": CLLocationManager.locationServicesEnabled(),
|
|
"backgroundModeEnabled": true
|
|
]
|
|
}
|
|
|
|
AsyncFunction("getCurrentPositionAsync") { (options: LocationOptions) -> [String: Any] in
|
|
try ensureForegroundLocationPermissions(appContext)
|
|
|
|
let requester = await LocationRequester(options: options)
|
|
let location = try await requester.requestLocation()
|
|
|
|
return exportLocation(location)
|
|
}
|
|
|
|
AsyncFunction("watchPositionImplAsync") { (watchId: Int, options: LocationOptions) in
|
|
try ensureForegroundLocationPermissions(appContext)
|
|
|
|
let streamer = await LocationsStreamer(options: options)
|
|
|
|
locationStreamers[watchId] = streamer
|
|
|
|
// Start streaming in another task, so the returned promise is not waiting for the stream to end.
|
|
Task {
|
|
do {
|
|
for try await locations in try streamer.streamLocations() {
|
|
guard let location = locations.last else {
|
|
continue
|
|
}
|
|
sendEvent(EVENT_LOCATION_CHANGED, [
|
|
"watchId": watchId,
|
|
"location": exportLocation(location)
|
|
])
|
|
}
|
|
} catch let exception as Exception {
|
|
sendEvent(EVENT_LOCATION_ERROR, ["watchId": watchId, "reason": exception.reason])
|
|
} catch {
|
|
sendEvent(EVENT_LOCATION_ERROR, ["watchId": watchId, "reason": error.localizedDescription])
|
|
}
|
|
}
|
|
}
|
|
|
|
AsyncFunction("getLastKnownPositionAsync") { (requirements: LastKnownLocationRequirements) -> [String: Any]? in
|
|
try ensureForegroundLocationPermissions(appContext)
|
|
|
|
if let location = CLLocationManager().location, isLocation(location, valid: requirements) {
|
|
return exportLocation(location)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
AsyncFunction("watchDeviceHeading") { (watchId: Int) in
|
|
try ensureForegroundLocationPermissions(appContext)
|
|
|
|
let options = LocationOptions(accuracy: .bestForNavigation, distanceInterval: 0)
|
|
let streamer = await DeviceHeadingStreamer(options: options)
|
|
|
|
locationStreamers[watchId] = streamer
|
|
|
|
// Start streaming in another task, so the returned promise is not waiting for the stream to end.
|
|
Task {
|
|
do {
|
|
for try await heading in try streamer.streamDeviceHeading() {
|
|
sendEvent(EVENT_HEADING_CHANGED, [
|
|
"watchId": watchId,
|
|
"heading": [
|
|
"trueHeading": heading.trueHeading,
|
|
"magHeading": heading.magneticHeading,
|
|
"accuracy": normalizeAccuracy(heading.headingAccuracy)
|
|
]
|
|
])
|
|
}
|
|
} catch let exception as Exception {
|
|
sendEvent(EVENT_LOCATION_ERROR, ["watchId": watchId, "reason": exception.reason])
|
|
} catch {
|
|
sendEvent(EVENT_LOCATION_ERROR, ["watchId": watchId, "reason": error.localizedDescription])
|
|
}
|
|
}
|
|
}
|
|
|
|
AsyncFunction("removeWatchAsync") { (watchId: Int) in
|
|
if let streamer = locationStreamers[watchId] {
|
|
streamer.stopStreaming()
|
|
}
|
|
locationStreamers[watchId] = nil
|
|
}
|
|
|
|
AsyncFunction("geocodeAsync") { (address: String) in
|
|
return try await Geocoder.geocode(address: address)
|
|
}
|
|
|
|
AsyncFunction("reverseGeocodeAsync") { (location: CLLocation) in
|
|
return try await Geocoder.reverseGeocode(location: location)
|
|
}
|
|
|
|
AsyncFunction("getPermissionsAsync") { (promise: Promise) in
|
|
try getPermissionUsingRequester(EXLocationPermissionRequester.self, appContext: appContext, promise: promise)
|
|
}
|
|
|
|
AsyncFunction("requestPermissionsAsync") { (promise: Promise) in
|
|
try askForPermissionUsingRequester(EXLocationPermissionRequester.self, appContext: appContext, promise: promise)
|
|
}
|
|
|
|
AsyncFunction("getForegroundPermissionsAsync") { (promise: Promise) in
|
|
try getPermissionUsingRequester(EXForegroundPermissionRequester.self, appContext: appContext, promise: promise)
|
|
}
|
|
|
|
AsyncFunction("requestForegroundPermissionsAsync") { (promise: Promise) in
|
|
try askForPermissionUsingRequester(EXForegroundPermissionRequester.self, appContext: appContext, promise: promise)
|
|
}
|
|
|
|
AsyncFunction("getBackgroundPermissionsAsync") { (promise: Promise) in
|
|
try getPermissionUsingRequester(EXBackgroundLocationPermissionRequester.self, appContext: appContext, promise: promise)
|
|
}
|
|
|
|
AsyncFunction("requestBackgroundPermissionsAsync") { (promise: Promise) in
|
|
try askForPermissionUsingRequester(EXBackgroundLocationPermissionRequester.self, appContext: appContext, promise: promise)
|
|
}
|
|
|
|
AsyncFunction("hasServicesEnabledAsync") {
|
|
return CLLocationManager.locationServicesEnabled()
|
|
}
|
|
|
|
// Background location
|
|
|
|
AsyncFunction("startLocationUpdatesAsync") { (taskName: String, options: [String: Any]) in
|
|
// There are two ways of starting this service.
|
|
// 1. As a background location service, this requires the background location permission.
|
|
// 2. As a user-initiated foreground service, this does NOT require the background location permission.
|
|
// Unfortunately, we cannot distinguish between those cases.
|
|
// So we only check foreground permission which needs to be granted in both cases.
|
|
try ensureLocationServicesEnabled()
|
|
try ensureForegroundLocationPermissions(appContext)
|
|
|
|
guard CLLocationManager.significantLocationChangeMonitoringAvailable() else {
|
|
throw Exceptions.LocationUpdatesUnavailable()
|
|
}
|
|
guard try taskManager.hasBackgroundModeEnabled("location") else {
|
|
throw Exceptions.LocationUpdatesUnavailable()
|
|
}
|
|
|
|
try taskManager.registerTask(withName: taskName, consumer: EXLocationTaskConsumer.self, options: options)
|
|
}
|
|
|
|
AsyncFunction("stopLocationUpdatesAsync") { (taskName: String) in
|
|
let taskManager = try taskManager
|
|
|
|
try EXUtilities.catchException {
|
|
taskManager.unregisterTask(withName: taskName, consumerClass: EXLocationTaskConsumer.self)
|
|
}
|
|
}
|
|
|
|
AsyncFunction("hasStartedLocationUpdatesAsync") { (taskName: String) -> Bool in
|
|
return try taskManager.task(withName: taskName, hasConsumerOf: EXLocationTaskConsumer.self)
|
|
}
|
|
|
|
// Geofencing
|
|
|
|
AsyncFunction("startGeofencingAsync") { (taskName: String, options: [String: Any]) in
|
|
try ensureBackgroundLocationPermissions(appContext)
|
|
|
|
guard CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) else {
|
|
throw Exceptions.GeofencingUnavailable()
|
|
}
|
|
guard try taskManager.hasBackgroundModeEnabled("location") else {
|
|
throw Exceptions.LocationUpdatesUnavailable()
|
|
}
|
|
|
|
try taskManager.registerTask(withName: taskName, consumer: EXGeofencingTaskConsumer.self, options: options)
|
|
}
|
|
|
|
AsyncFunction("stopGeofencingAsync") { (taskName: String) in
|
|
let taskManager = try taskManager
|
|
|
|
try EXUtilities.catchException {
|
|
taskManager.unregisterTask(withName: taskName, consumerClass: EXGeofencingTaskConsumer.self)
|
|
}
|
|
}
|
|
|
|
AsyncFunction("hasStartedGeofencingAsync") { (taskName: String) -> Bool in
|
|
return try taskManager.task(withName: taskName, hasConsumerOf: EXGeofencingTaskConsumer.self)
|
|
}
|
|
}
|
|
}
|