tauri-plugin-geolocation 2.3.2

Get and track the device's current position
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

import CoreLocation
import SwiftRs
import Tauri
import UIKit
import WebKit

class GetPositionArgs: Decodable {
  var enableHighAccuracy: Bool?
}

class WatchPositionArgs: Decodable {
  let options: GetPositionArgs
  let channel: Channel
}

class ClearWatchArgs: Decodable {
  let channelId: UInt32
}

class GeolocationPlugin: Plugin, CLLocationManagerDelegate {
  private let locationManager = CLLocationManager()
  private var isUpdatingLocation: Bool = false
  private var permissionRequests: [Invoke] = []
  private var positionRequests: [Invoke] = []
  private var watcherChannels: [Channel] = []

  override init() {
    super.init()
    locationManager.delegate = self
  }

  //
  // Tauri commands
  //

  @objc public func getCurrentPosition(_ invoke: Invoke) throws {
    let args = try invoke.parseArgs(GetPositionArgs.self)

    self.positionRequests.append(invoke)

    DispatchQueue.main.async {
      if args.enableHighAccuracy == true {
        self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
      } else {
        self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
      }

      // TODO: Use the authorizationStatus instance property with locationManagerDidChangeAuthorization(_:) instead.
      if CLLocationManager.authorizationStatus() == .notDetermined {
        self.locationManager.requestWhenInUseAuthorization()
      } else {
        self.locationManager.requestLocation()
      }
    }
  }

  @objc public func watchPosition(_ invoke: Invoke) throws {
    let args = try invoke.parseArgs(WatchPositionArgs.self)

    self.watcherChannels.append(args.channel)

    DispatchQueue.main.async {
      if args.options.enableHighAccuracy == true {
        self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
      } else {
        self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
      }

      // TODO: Use the authorizationStatus instance property with locationManagerDidChangeAuthorization(_:) instead.
      if CLLocationManager.authorizationStatus() == .notDetermined {
        self.locationManager.requestWhenInUseAuthorization()
      } else {
        self.locationManager.startUpdatingLocation()
        self.isUpdatingLocation = true
      }
    }

    invoke.resolve()
  }

  @objc public func clearWatch(_ invoke: Invoke) throws {
    let args = try invoke.parseArgs(ClearWatchArgs.self)

    self.watcherChannels = self.watcherChannels.filter { $0.id != args.channelId }

    // TODO: capacitor plugin calls stopUpdating unconditionally
    if self.watcherChannels.isEmpty {
      self.stopUpdating()
    }

    invoke.resolve()
  }

  @objc override public func checkPermissions(_ invoke: Invoke) {
    var status: String = ""

    if CLLocationManager.locationServicesEnabled() {
      // TODO: Use the authorizationStatus instance property with locationManagerDidChangeAuthorization(_:) instead.
      switch CLLocationManager.authorizationStatus() {
      case .notDetermined:
        status = "prompt"
      case .restricted, .denied:
        status = "denied"
      case .authorizedAlways, .authorizedWhenInUse:
        status = "granted"
      @unknown default:
        status = "prompt"
      }
    } else {
      invoke.reject("Location services are not enabled.")
      return
    }

    let result = ["location": status, "coarseLocation": status]

    invoke.resolve(result)
  }

  @objc override public func requestPermissions(_ invoke: Invoke) {
    if CLLocationManager.locationServicesEnabled() {
      // TODO: Use the authorizationStatus instance property with locationManagerDidChangeAuthorization(_:) instead.
      if CLLocationManager.authorizationStatus() == .notDetermined {
        self.permissionRequests.append(invoke)

        DispatchQueue.main.async {
          self.locationManager.requestWhenInUseAuthorization()
        }
      } else {
        checkPermissions(invoke)
      }
    } else {
      invoke.reject("Location services are not enabled.")
    }
  }

  //
  // Delegate methods
  //

  public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    Logger.error(error)

    let requests = self.positionRequests + self.permissionRequests
    self.positionRequests.removeAll()
    self.permissionRequests.removeAll()

    for request in requests {
      request.reject(error.localizedDescription)
    }

    for channel in self.watcherChannels {
      do {
        try channel.send(error.localizedDescription)
      } catch {
        Logger.error(error)
      }
    }
  }

  public func locationManager(
    _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]
  ) {
    // Respond to all getCurrentPosition() calls.
    for request in self.positionRequests {
      // The capacitor plugin uses locations.first but .last should be the most current one
      // and i don't see a reason to use old locations
      if let location = locations.last {
        let result = convertLocation(location)
        request.resolve(result)
      } else {
        request.reject("Location service returned an empty Location array.")
      }
    }

    for channel in self.watcherChannels {
      // The capacitor plugin uses locations.first but .last should be the most recent one
      // and i don't see a reason to use old locations
      if let location = locations.last {
        let result = convertLocation(location)
        do {
          try channel.send(result)
        } catch {
          Logger.error(error)
        }
      } else {
        do {
          try channel.send("Location service returned an empty Location array.")
        } catch {
          Logger.error(error)
        }
      }
    }
  }

  public func locationManager(
    _ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus
  ) {
    let requests = self.permissionRequests
    self.permissionRequests.removeAll()

    for request in requests {
      checkPermissions(request)
    }

    if !self.positionRequests.isEmpty {
      self.locationManager.requestLocation()
    }

    if !self.watcherChannels.isEmpty && !self.isUpdatingLocation {
      self.locationManager.startUpdatingLocation()
      self.isUpdatingLocation = true
    }
  }

  //
  // Internal/Helper methods
  //

  // TODO: Why is this pub in capacitor
  private func stopUpdating() {
    self.locationManager.stopUpdatingLocation()
    self.isUpdatingLocation = false
  }

  private func convertLocation(_ location: CLLocation) -> JsonObject {
    var ret: JsonObject = [:]
    var coords: JsonObject = [:]

    coords["latitude"] = location.coordinate.latitude
    coords["longitude"] = location.coordinate.longitude
    coords["accuracy"] = location.horizontalAccuracy
    coords["altitude"] = location.altitude
    coords["altitudeAccuracy"] = location.verticalAccuracy
    coords["speed"] = location.speed
    coords["heading"] = location.course
    ret["timestamp"] = Int((location.timestamp.timeIntervalSince1970 * 1000))
    ret["coords"] = coords

    return ret
  }
}

@_cdecl("init_plugin_geolocation")
func initPlugin() -> Plugin {
  return GeolocationPlugin()
}