feat: Add geolocation and haptics plugins (#1599)

* init geolocation plugin

* ios impl (w/o js api)

* generate ts api

* use newer tauri commit

* add temporary postinstall

* include src in files

* guest-js

* just ship dist-js for now

* fix watcher

* fix android compile error

* fix android build for real

* fix heading type

* initial getCurrentPosition android impl (wip)

* prevent panics if errors (strings) are sent over the channel

* Add android watchPosition implementation

* init haptics plugin (android)

* ios and new apis (ANDROID IS LIKELY BROKEN - MAY NOT EVEN COMPILE)

* use tauri-specta that accounts for raw fn arg idents

* add complete android support (it's not working great due to random soft-/hardware support)

* fix(haptics): Fix the NotificationFeedbackType::Success and Version (#1)

* Fix success feedback and version

* Apply suggestions from code review

* Update package.json

---------

Co-authored-by: Fabian-Lars <118197967+FabianLars-crabnebula@users.noreply.github.com>

* android: improve permission callback handling

* keep track of ongoing perms requests

* rebuild

* license headers

* rm sqlite feat

* fmt

* what diff u talkin bout?

* ignore dist-js again

* fix audits

* dedupe api.js

* clippy

* changefiles

* readmes

* clean up todos

* rm dsstore

* rm wrong feats

* mirror

* covector

* rebuild

* ios requires the wry feature

* lint

* update lock

---------

Co-authored-by: fabianlars <fabianlars@fabianlars.de>
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
Co-authored-by: Naman Garg <155433377+naman-crabnebula@users.noreply.github.com>
Co-authored-by: Lucas Nogueira <lucas@crabnebula.dev>
This commit is contained in:
Fabian-Lars
2024-08-02 15:45:47 +02:00
committed by GitHub
parent 34df132fb1
commit 9606089b2a
102 changed files with 4896 additions and 52 deletions
+10
View File
@@ -0,0 +1,10 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Package.resolved
+33
View File
@@ -0,0 +1,33 @@
// swift-tools-version:5.3
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import PackageDescription
let package = Package(
name: "tauri-plugin-geolocation",
platforms: [
.iOS(.v13),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "tauri-plugin-geolocation",
type: .static,
targets: ["tauri-plugin-geolocation"]),
],
dependencies: [
.package(name: "Tauri", path: "../.tauri/tauri-api")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "tauri-plugin-geolocation",
dependencies: [
.byName(name: "Tauri")
],
path: "Sources")
]
)
+3
View File
@@ -0,0 +1,3 @@
# Tauri Plugin Geolocation
A description of this package.
@@ -0,0 +1,246 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import SwiftRs
import Tauri
import UIKit
import WebKit
import CoreLocation
class GetPositionArgs: Decodable {
let 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()
}
@@ -0,0 +1,12 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import XCTest
@testable import ExamplePlugin
final class ExamplePluginTests: XCTestCase {
func testExample() throws {
let plugin = ExamplePlugin()
}
}