This commit is contained in:
metacryst
2026-01-09 11:14:27 -06:00
parent cf03c95664
commit 637c9e4674
2149 changed files with 527743 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
import Capacitor
import IONGeolocationLib
private enum GeolocationCallbackType {
case requestPermissions
case location
case watch
var shouldKeepCallback: Bool {
self == .watch
}
var shouldClearAfterSending: Bool {
self == .location || self == .requestPermissions
}
}
private struct GeolocationCallbackGroup {
let ids: [CAPPluginCall]
let type: GeolocationCallbackType
}
final class GeolocationCallbackManager {
private(set) var requestPermissionsCallbacks: [CAPPluginCall]
private(set) var locationCallbacks: [CAPPluginCall]
private(set) var watchCallbacks: [String: CAPPluginCall]
private let capacitorBridge: CAPBridgeProtocol?
private var allCallbackGroups: [GeolocationCallbackGroup] {
[
.init(ids: requestPermissionsCallbacks, type: .requestPermissions),
.init(ids: locationCallbacks, type: .location),
.init(ids: Array(watchCallbacks.values), type: .watch)
]
}
private var requestPermissionsCallbackGroup: GeolocationCallbackGroup? {
allCallbackGroups.first { $0.type == .requestPermissions }
}
init(capacitorBridge: CAPBridgeProtocol?) {
self.capacitorBridge = capacitorBridge
self.requestPermissionsCallbacks = []
self.locationCallbacks = []
self.watchCallbacks = [:]
}
func addRequestPermissionsCallback(capacitorCall call: CAPPluginCall) {
capacitorBridge?.saveCall(call)
requestPermissionsCallbacks.append(call)
}
func addLocationCallback(capacitorCall call: CAPPluginCall) {
capacitorBridge?.saveCall(call)
locationCallbacks.append(call)
}
func addWatchCallback(_ watchId: String, capacitorCall call: CAPPluginCall) {
capacitorBridge?.saveCall(call)
watchCallbacks[watchId] = call
}
func clearRequestPermissionsCallbacks() {
requestPermissionsCallbacks.forEach {
capacitorBridge?.releaseCall($0)
}
requestPermissionsCallbacks.removeAll()
}
func clearWatchCallbackIfExists(_ watchId: String) {
if let callbackToRemove = watchCallbacks[watchId] {
capacitorBridge?.releaseCall(callbackToRemove)
watchCallbacks.removeValue(forKey: watchId)
}
}
func clearLocationCallbacks() {
locationCallbacks.forEach {
capacitorBridge?.releaseCall($0)
}
locationCallbacks.removeAll()
}
func sendSuccess(_ call: CAPPluginCall) {
call.resolve()
}
func sendSuccess(_ call: CAPPluginCall, with data: PluginCallResultData) {
call.resolve(data)
}
func sendRequestPermissionsSuccess(_ permissionsResult: String) {
if let group = requestPermissionsCallbackGroup {
let data = [
Constants.AuthorisationStatus.ResultKey.location: permissionsResult,
Constants.AuthorisationStatus.ResultKey.coarseLocation: permissionsResult
]
send(.success(data), to: group)
}
}
func sendSuccess(with position: IONGLOCPositionModel) {
createPluginResult(status: .success(position.toJSObject()))
}
func sendError(_ call: CAPPluginCall, error: GeolocationError) {
let errorModel = error.toCodeMessagePair()
call.reject(errorModel.1, errorModel.0)
}
func sendError(_ error: GeolocationError) {
createPluginResult(status: .error(error.toCodeMessagePair()))
}
}
private enum CallResultStatus {
typealias SuccessModel = JSObject
typealias ErrorModel = (code: String, message: String)
case success(_ data: SuccessModel)
case error(_ codeAndMessage: ErrorModel)
}
private extension GeolocationCallbackManager {
func createPluginResult(status: CallResultStatus) {
allCallbackGroups.forEach {
send(status, to: $0)
}
}
func send(_ callResultStatus: CallResultStatus, to group: GeolocationCallbackGroup) {
group.ids.forEach { call in
call.keepAlive = group.type.shouldKeepCallback
switch callResultStatus {
case .success(let data):
call.resolve(data)
case .error(let error):
call.reject(error.message, error.code)
}
}
if group.type.shouldClearAfterSending {
clearCallbacks(for: group.type)
}
}
func clearCallbacks(for type: GeolocationCallbackType) {
if case .location = type {
clearLocationCallbacks()
} else if case .requestPermissions = type {
clearRequestPermissionsCallbacks()
}
}
}

View File

@@ -0,0 +1,36 @@
enum Constants {
enum Arguments {
static let enableHighAccuracy = "enableHighAccuracy"
static let id = "id"
}
enum AuthorisationStatus {
enum ResultKey {
static let location = "location"
static let coarseLocation = "coarseLocation"
}
enum Status {
static let denied: String = "denied"
static let granted: String = "granted"
static let prompt: String = "prompt"
}
}
enum LocationUsageDescription {
static let always: String = "NSLocationAlwaysAndWhenInUseUsageDescription"
static let whenInUse: String = "NSLocationWhenInUseUsageDescription"
}
enum Position {
static let altitude: String = "altitude"
static let coords: String = "coords"
static let heading: String = "heading"
static let accuracy: String = "accuracy"
static let latitude: String = "latitude"
static let longitude: String = "longitude"
static let speed: String = "speed"
static let timestamp: String = "timestamp"
static let altitudeAccuracy: String = "altitudeAccuracy"
}
}

View File

@@ -0,0 +1,44 @@
enum GeolocationMethod: String {
case getCurrentPosition
case watchPosition
case clearWatch
}
enum GeolocationError: Error {
case locationServicesDisabled
case permissionDenied
case permissionRestricted
case positionUnavailable
case inputArgumentsIssue(target: GeolocationMethod)
func toCodeMessagePair() -> (String, String) {
("OS-PLUG-GLOC-\(String(format: "%04d", code))", description)
}
}
private extension GeolocationError {
var code: Int {
switch self {
case .positionUnavailable: 2
case .permissionDenied: 3
case .locationServicesDisabled: 7
case .permissionRestricted: 8
case .inputArgumentsIssue(let target):
switch target {
case .getCurrentPosition: 4
case .watchPosition: 5
case .clearWatch: 6
}
}
}
var description: String {
switch self {
case .positionUnavailable: "There was en error trying to obtain the location."
case .permissionDenied: "Location permission request was denied."
case .locationServicesDisabled: "Location services are not enabled."
case .permissionRestricted: "Application's use of location services was restricted."
case .inputArgumentsIssue(let target): "The '\(target.rawValue)' input parameters aren't valid."
}
}
}

View File

@@ -0,0 +1,228 @@
import Capacitor
import IONGeolocationLib
import UIKit
import Combine
@objc(GeolocationPlugin)
public class GeolocationPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "GeolocationPlugin"
public let jsName = "Geolocation"
public let pluginMethods: [CAPPluginMethod] = [
.init(name: "getCurrentPosition", returnType: CAPPluginReturnPromise),
.init(name: "watchPosition", returnType: CAPPluginReturnCallback),
.init(name: "clearWatch", returnType: CAPPluginReturnPromise),
.init(name: "checkPermissions", returnType: CAPPluginReturnPromise),
.init(name: "requestPermissions", returnType: CAPPluginReturnPromise)
]
private var locationService: (any IONGLOCService)?
private var cancellables = Set<AnyCancellable>()
private var locationCancellable: AnyCancellable?
private var callbackManager: GeolocationCallbackManager?
private var statusInitialized = false
private var locationInitialized: Bool = false
override public func load() {
self.locationService = IONGLOCManagerWrapper()
self.callbackManager = .init(capacitorBridge: bridge)
NotificationCenter.default.addObserver(
self,
selector: #selector(appDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
}
@objc private func appDidBecomeActive() {
if let watchCallbacksEmpty = callbackManager?.watchCallbacks.isEmpty, !watchCallbacksEmpty {
print("App became active. Restarting location monitoring for watch callbacks.")
locationCancellable?.cancel()
locationCancellable = nil
locationInitialized = false
locationService?.stopMonitoringLocation()
locationService?.startMonitoringLocation()
bindLocationPublisher()
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc func getCurrentPosition(_ call: CAPPluginCall) {
shouldSetupBindings()
let enableHighAccuracy = call.getBool(Constants.Arguments.enableHighAccuracy, false)
handleLocationRequest(enableHighAccuracy, call: call)
}
@objc func watchPosition(_ call: CAPPluginCall) {
shouldSetupBindings()
let enableHighAccuracy = call.getBool(Constants.Arguments.enableHighAccuracy, false)
let watchUUID = call.callbackId
handleLocationRequest(enableHighAccuracy, watchUUID: watchUUID, call: call)
}
@objc func clearWatch(_ call: CAPPluginCall) {
shouldSetupBindings()
guard let callbackId = call.getString(Constants.Arguments.id) else {
callbackManager?.sendError(.inputArgumentsIssue(target: .clearWatch))
return
}
callbackManager?.clearWatchCallbackIfExists(callbackId)
if (callbackManager?.watchCallbacks.isEmpty) ?? false {
locationService?.stopMonitoringLocation()
locationCancellable?.cancel()
locationCancellable = nil
locationInitialized = false
}
callbackManager?.sendSuccess(call)
}
@objc override public func checkPermissions(_ call: CAPPluginCall) {
guard checkIfLocationServicesAreEnabled(call) else { return }
let status = switch locationService?.authorisationStatus {
case .restricted, .denied: Constants.AuthorisationStatus.Status.denied
case .authorisedAlways, .authorisedWhenInUse: Constants.AuthorisationStatus.Status.granted
default: Constants.AuthorisationStatus.Status.prompt
}
let callResultData = [
Constants.AuthorisationStatus.ResultKey.location: status,
Constants.AuthorisationStatus.ResultKey.coarseLocation: status
]
callbackManager?.sendSuccess(call, with: callResultData)
}
@objc override public func requestPermissions(_ call: CAPPluginCall) {
guard checkIfLocationServicesAreEnabled(call) else { return }
if locationService?.authorisationStatus == .notDetermined {
shouldSetupBindings()
callbackManager?.addRequestPermissionsCallback(capacitorCall: call)
} else {
checkPermissions(call)
}
}
}
private extension GeolocationPlugin {
func shouldSetupBindings() {
bindAuthorisationStatusPublisher()
bindLocationPublisher()
}
func bindAuthorisationStatusPublisher() {
guard !statusInitialized else { return }
statusInitialized = true
locationService?.authorisationStatusPublisher
.sink(receiveValue: { [weak self] status in
guard let self else { return }
switch status {
case .denied:
self.onLocationPermissionNotGranted(error: .permissionDenied)
case .notDetermined:
self.requestLocationAuthorisation(type: .whenInUse)
case .restricted:
self.onLocationPermissionNotGranted(error: .permissionRestricted)
case .authorisedAlways, .authorisedWhenInUse:
self.onLocationPermissionGranted()
@unknown default: break
}
})
.store(in: &cancellables)
}
func bindLocationPublisher() {
guard !locationInitialized else { return }
locationInitialized = true
locationCancellable = locationService?.currentLocationPublisher
.catch { [weak self] error -> AnyPublisher<IONGLOCPositionModel, Never> in
print("An error was found while retrieving the location: \(error)")
if case IONGLOCLocationError.locationUnavailable = error {
print("Location unavailable (likely due to backgrounding). Keeping watch callbacks alive.")
self?.callbackManager?.sendError(.positionUnavailable)
return Empty<IONGLOCPositionModel, Never>()
.eraseToAnyPublisher()
} else {
self?.callbackManager?.sendError(.positionUnavailable)
return Empty<IONGLOCPositionModel, Never>()
.eraseToAnyPublisher()
}
}
.sink(receiveValue: { [weak self] position in
self?.callbackManager?.sendSuccess(with: position)
})
}
func requestLocationAuthorisation(type requestType: IONGLOCAuthorisationRequestType) {
DispatchQueue.global(qos: .background).async {
guard self.checkIfLocationServicesAreEnabled() else { return }
self.locationService?.requestAuthorisation(withType: requestType)
}
}
func checkIfLocationServicesAreEnabled(_ call: CAPPluginCall? = nil) -> Bool {
guard locationService?.areLocationServicesEnabled() == true else {
call.map { callbackManager?.sendError($0, error: .locationServicesDisabled) }
?? callbackManager?.sendError(.locationServicesDisabled)
return false
}
return true
}
func onLocationPermissionNotGranted(error: GeolocationError) {
let shouldNotifyRequestPermissionsResult = callbackManager?.requestPermissionsCallbacks.isEmpty == false
let shouldNotifyPermissionError = callbackManager?.locationCallbacks.isEmpty == false || callbackManager?.watchCallbacks.isEmpty == false
if shouldNotifyRequestPermissionsResult {
self.callbackManager?.sendRequestPermissionsSuccess(Constants.AuthorisationStatus.Status.denied)
}
if shouldNotifyPermissionError {
self.callbackManager?.sendError(error)
}
}
func onLocationPermissionGranted() {
let shouldNotifyPermissionGranted = callbackManager?.requestPermissionsCallbacks.isEmpty == false
// should request location if callbacks below exist and are not empty
let shouldRequestCurrentPosition = callbackManager?.locationCallbacks.isEmpty == false
let shouldRequestLocationMonitoring = callbackManager?.watchCallbacks.isEmpty == false
if shouldNotifyPermissionGranted {
callbackManager?.sendRequestPermissionsSuccess(Constants.AuthorisationStatus.Status.granted)
}
if shouldRequestCurrentPosition {
locationService?.requestSingleLocation()
}
if shouldRequestLocationMonitoring {
locationService?.startMonitoringLocation()
}
}
func handleLocationRequest(_ enableHighAccuracy: Bool, watchUUID: String? = nil, call: CAPPluginCall) {
bindLocationPublisher()
let configurationModel = IONGLOCConfigurationModel(enableHighAccuracy: enableHighAccuracy)
locationService?.updateConfiguration(configurationModel)
if let watchUUID {
callbackManager?.addWatchCallback(watchUUID, capacitorCall: call)
} else {
callbackManager?.addLocationCallback(capacitorCall: call)
}
switch locationService?.authorisationStatus {
case .authorisedAlways, .authorisedWhenInUse: onLocationPermissionGranted()
case .denied: callbackManager?.sendError(.permissionDenied)
case .restricted: callbackManager?.sendError(.permissionRestricted)
default: break
}
}
}

View File

@@ -0,0 +1,23 @@
import Capacitor
import IONGeolocationLib
extension IONGLOCPositionModel {
func toJSObject() -> JSObject {
[
Constants.Position.timestamp: timestamp,
Constants.Position.coords: coordsJSObject
]
}
private var coordsJSObject: JSObject {
[
Constants.Position.altitude: altitude,
Constants.Position.heading: course,
Constants.Position.accuracy: horizontalAccuracy,
Constants.Position.latitude: latitude,
Constants.Position.longitude: longitude,
Constants.Position.speed: speed,
Constants.Position.altitudeAccuracy: verticalAccuracy
]
}
}