@azatek/background-geolocation 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Location accuracy levels for power management.
3
+ * Higher accuracy = more battery consumption.
4
+ */
5
+ export enum LocationAccuracy {
6
+ /**
7
+ * Highest accuracy (GPS) - High battery consumption
8
+ * Android: PRIORITY_HIGH_ACCURACY
9
+ * iOS: kCLLocationAccuracyBest
10
+ */
11
+ HIGH = 100,
12
+
13
+ /**
14
+ * Balanced accuracy (GPS + Network) - Medium battery consumption
15
+ * Android: PRIORITY_BALANCED_POWER_ACCURACY
16
+ * iOS: kCLLocationAccuracyNearestTenMeters
17
+ */
18
+ BALANCED = 102,
19
+
20
+ /**
21
+ * Low accuracy (Network) - Low battery consumption
22
+ * Android: PRIORITY_LOW_POWER
23
+ * iOS: kCLLocationAccuracyHundredMeters
24
+ */
25
+ LOW = 104,
26
+
27
+ /**
28
+ * Passive updates - Minimal battery consumption
29
+ * Android: PRIORITY_PASSIVE
30
+ * iOS: kCLLocationAccuracyThreeKilometers
31
+ */
32
+ PASSIVE = 105
33
+ }
34
+
35
+ /**
36
+ * The options for configuring a watcher that listens for location updates.
37
+ */
38
+ export interface WatcherOptions {
39
+ /**
40
+ * If the "backgroundMessage" option is defined, the watcher will
41
+ * provide location updates whether the app is in the background or the
42
+ * foreground. If it is not defined, location updates are only
43
+ * guaranteed in the foreground. This is true on both platforms.
44
+ *
45
+ * On Android, a notification must be shown to continue receiving
46
+ * location updates in the background. This option specifies the text of
47
+ * that notification.
48
+ */
49
+ backgroundMessage?: string;
50
+ /**
51
+ * The title of the notification mentioned above.
52
+ * @default "Using your location"
53
+ */
54
+ backgroundTitle?: string;
55
+ /**
56
+ * Whether permissions should be requested from the user automatically,
57
+ * if they are not already granted.
58
+ * @default true
59
+ */
60
+ requestPermissions?: boolean;
61
+ /**
62
+ * If "true", stale locations may be delivered while the device
63
+ * obtains a GPS fix. You are responsible for checking the "time"
64
+ * property. If "false", locations are guaranteed to be up to date.
65
+ * @default false
66
+ */
67
+ stale?: boolean;
68
+ /**
69
+ * The distance in meters that the device must move before a new location update is triggered.
70
+ * This is used to filter out small movements and reduce the number of updates.
71
+ * @default 0
72
+ */
73
+ distanceFilter?: number;
74
+ /**
75
+ * The desired accuracy level for location updates.
76
+ * Higher accuracy levels consume more battery power.
77
+ * @default LocationAccuracy.BALANCED
78
+ */
79
+ accuracy?: LocationAccuracy;
80
+ }
81
+
82
+ /**
83
+ * Represents a geographical location with various attributes.
84
+ */
85
+ export interface Location {
86
+ /**
87
+ * Longitude in degrees.
88
+ */
89
+ latitude: number;
90
+ /**
91
+ * Latitude in degrees.
92
+ */
93
+ longitude: number;
94
+ /**
95
+ * Radius of horizontal uncertainty in metres, with 68% confidence.
96
+ */
97
+ accuracy: number;
98
+ /**
99
+ * Metres above sea level (or null).
100
+ */
101
+ altitude: number | null;
102
+ /**
103
+ * Vertical uncertainty in metres, with 68% confidence (or null).
104
+ */
105
+ altitudeAccuracy: number | null;
106
+ /**
107
+ * `true` if the location was simulated by software, rather than GPS.
108
+ */
109
+ simulated: boolean;
110
+ /**
111
+ * Deviation from true north in degrees (or null).
112
+ */
113
+ bearing: number | null;
114
+ /**
115
+ * Speed in metres per second (or null).
116
+ */
117
+ speed: number | null;
118
+ /**
119
+ * Time the location was produced, in milliseconds since the unix epoch.
120
+ */
121
+ time: number | null;
122
+ }
123
+
124
+ export interface CallbackError extends Error {
125
+ code?: string;
126
+ }
127
+
128
+ export interface BackgroundGeolocationPlugin {
129
+ /**
130
+ * Adds a watcher for location updates.
131
+ * The watcher will be invoked with the latest location whenever it is available.
132
+ * If an error occurs, the callback will be invoked with the error.
133
+ *
134
+ * @param options the watcher options
135
+ * @param callback the callback to be invoked when a new location is available or an error occurs
136
+ * @returns a promise that resolves to a unique identifier for the watcher ID
137
+ */
138
+ addWatcher(
139
+ options: WatcherOptions,
140
+ callback: (
141
+ position?: Location,
142
+ error?: CallbackError
143
+ ) => void
144
+ ): Promise<string>;
145
+ /**
146
+ * Removes a watcher by its unique identifier.
147
+ * @param options the options for removing the watcher
148
+ * @returns a promise that resolves when the watcher is successfully removed
149
+ */
150
+ removeWatcher(options: {
151
+ id: string
152
+ }): Promise<void>;
153
+ /**
154
+ * Opens the settings page of the app.
155
+ */
156
+ openSettings(): Promise<void>;
157
+ }
@@ -0,0 +1,24 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
7
+ <key>CFBundleExecutable</key>
8
+ <string>$(EXECUTABLE_NAME)</string>
9
+ <key>CFBundleIdentifier</key>
10
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11
+ <key>CFBundleInfoDictionaryVersion</key>
12
+ <string>6.0</string>
13
+ <key>CFBundleName</key>
14
+ <string>$(PRODUCT_NAME)</string>
15
+ <key>CFBundlePackageType</key>
16
+ <string>FMWK</string>
17
+ <key>CFBundleShortVersionString</key>
18
+ <string>1.0</string>
19
+ <key>CFBundleVersion</key>
20
+ <string>$(CURRENT_PROJECT_VERSION)</string>
21
+ <key>NSPrincipalClass</key>
22
+ <string></string>
23
+ </dict>
24
+ </plist>
@@ -0,0 +1,10 @@
1
+ #import <UIKit/UIKit.h>
2
+
3
+ //! Project version number for Plugin.
4
+ FOUNDATION_EXPORT double PluginVersionNumber;
5
+
6
+ //! Project version string for Plugin.
7
+ FOUNDATION_EXPORT const unsigned char PluginVersionString[];
8
+
9
+ // In this header, you should import all the public headers of your framework using statements like #import <Plugin/PublicHeader.h>
10
+
@@ -0,0 +1,8 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <Capacitor/Capacitor.h>
3
+
4
+ CAP_PLUGIN(BackgroundGeolocation, "BackgroundGeolocation",
5
+ CAP_PLUGIN_METHOD(addWatcher, CAPPluginReturnCallback);
6
+ CAP_PLUGIN_METHOD(removeWatcher, CAPPluginReturnPromise);
7
+ CAP_PLUGIN_METHOD(openSettings, CAPPluginReturnPromise);
8
+ )
@@ -0,0 +1,263 @@
1
+ import Capacitor
2
+ import Foundation
3
+ import UIKit
4
+ import CoreLocation
5
+
6
+ // Avoids a bewildering type warning.
7
+ let null = Optional<Double>.none as Any
8
+
9
+ func formatLocation(_ location: CLLocation) -> PluginCallResultData {
10
+ var simulated = false;
11
+ if #available(iOS 15, *) {
12
+ // Prior to iOS 15, it was not possible to detect simulated locations.
13
+ // But in general, it is very difficult to simulate locations on iOS in
14
+ // production.
15
+ if location.sourceInformation != nil {
16
+ simulated = location.sourceInformation!.isSimulatedBySoftware;
17
+ }
18
+ }
19
+ return [
20
+ "latitude": location.coordinate.latitude,
21
+ "longitude": location.coordinate.longitude,
22
+ "accuracy": location.horizontalAccuracy,
23
+ "altitude": location.altitude,
24
+ "altitudeAccuracy": location.verticalAccuracy,
25
+ "simulated": simulated,
26
+ "speed": location.speed < 0 ? null : location.speed,
27
+ "bearing": location.course < 0 ? null : location.course,
28
+ "time": NSNumber(
29
+ value: Int(
30
+ location.timestamp.timeIntervalSince1970 * 1000
31
+ )
32
+ ),
33
+ ]
34
+ }
35
+
36
+ class Watcher {
37
+ let callbackId: String
38
+ let locationManager: CLLocationManager = CLLocationManager()
39
+ private let created = Date()
40
+ private let allowStale: Bool
41
+ private var isUpdatingLocation: Bool = false
42
+ init(_ id: String, stale: Bool) {
43
+ callbackId = id
44
+ allowStale = stale
45
+ }
46
+ func start() {
47
+ // Avoid unnecessary calls to startUpdatingLocation, which can
48
+ // result in extraneous invocations of didFailWithError.
49
+ if !isUpdatingLocation {
50
+ locationManager.startUpdatingLocation()
51
+ isUpdatingLocation = true
52
+ }
53
+ }
54
+ func stop() {
55
+ if isUpdatingLocation {
56
+ locationManager.stopUpdatingLocation()
57
+ isUpdatingLocation = false
58
+ }
59
+ }
60
+ func isLocationValid(_ location: CLLocation) -> Bool {
61
+ return (
62
+ allowStale ||
63
+ location.timestamp >= created
64
+ )
65
+ }
66
+ }
67
+
68
+ @objc(BackgroundGeolocation)
69
+ public class BackgroundGeolocation: CAPPlugin,
70
+ CAPBridgedPlugin,
71
+ CLLocationManagerDelegate {
72
+ private var watchers = [Watcher]()
73
+ public let identifier = "BackgroundGeolocation"
74
+ public let jsName = "BackgroundGeolocation"
75
+ public let pluginMethods: [CAPPluginMethod] = [
76
+ CAPPluginMethod(name: "addWatcher", returnType: CAPPluginReturnCallback),
77
+ CAPPluginMethod(name: "removeWatcher", returnType: CAPPluginReturnPromise),
78
+ CAPPluginMethod(name: "openSettings", returnType: CAPPluginReturnPromise)
79
+ ]
80
+
81
+ @objc public override func load() {
82
+ UIDevice.current.isBatteryMonitoringEnabled = true
83
+ }
84
+
85
+ @objc func addWatcher(_ call: CAPPluginCall) {
86
+ call.keepAlive = true
87
+
88
+ // CLLocationManager requires main thread
89
+ DispatchQueue.main.async {
90
+ let background = call.getString("backgroundMessage") != nil
91
+ let watcher = Watcher(
92
+ call.callbackId,
93
+ stale: call.getBool("stale") ?? false
94
+ )
95
+ let manager = watcher.locationManager
96
+ manager.delegate = self
97
+
98
+ // Configure accuracy based on parameter (default: BALANCED = 102)
99
+ let accuracy = call.getInt("accuracy") ?? 102
100
+
101
+ switch accuracy {
102
+ case 100: // HIGH
103
+ let isCharging = [
104
+ .charging,
105
+ .full
106
+ ].contains(UIDevice.current.batteryState)
107
+
108
+ if isCharging {
109
+ manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
110
+ } else {
111
+ manager.desiredAccuracy = kCLLocationAccuracyBest
112
+ }
113
+ manager.activityType = .otherNavigation
114
+ case 102: // BALANCED (Default)
115
+ manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
116
+ manager.activityType = .other
117
+ case 104: // LOW
118
+ manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
119
+ manager.activityType = .other
120
+ case 105: // PASSIVE
121
+ manager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
122
+ manager.activityType = .other
123
+ default:
124
+ manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
125
+ manager.activityType = .other
126
+ }
127
+ var distanceFilter = call.getDouble("distanceFilter")
128
+ // It appears that setting manager.distanceFilter to 0 can prevent
129
+ // subsequent location updates. See issue #88.
130
+ if distanceFilter == nil || distanceFilter == 0 {
131
+ distanceFilter = kCLDistanceFilterNone
132
+ }
133
+ manager.distanceFilter = distanceFilter!
134
+ manager.allowsBackgroundLocationUpdates = background
135
+ manager.showsBackgroundLocationIndicator = background
136
+ manager.pausesLocationUpdatesAutomatically = false
137
+ self.watchers.append(watcher)
138
+ if call.getBool("requestPermissions") != false {
139
+ let status = CLLocationManager.authorizationStatus()
140
+ if [
141
+ .notDetermined,
142
+ .denied,
143
+ .restricted,
144
+ ].contains(status) {
145
+ return (
146
+ background
147
+ ? manager.requestAlwaysAuthorization()
148
+ : manager.requestWhenInUseAuthorization()
149
+ )
150
+ }
151
+ if (
152
+ background && status == .authorizedWhenInUse
153
+ ) {
154
+ // Attempt to escalate.
155
+ manager.requestAlwaysAuthorization()
156
+ }
157
+ }
158
+ return watcher.start()
159
+ }
160
+ }
161
+
162
+ @objc func removeWatcher(_ call: CAPPluginCall) {
163
+ // CLLocationManager requires main thread
164
+ DispatchQueue.main.async {
165
+ if let callbackId = call.getString("id") {
166
+ if let index = self.watchers.firstIndex(
167
+ where: { $0.callbackId == callbackId }
168
+ ) {
169
+ self.watchers[index].locationManager.stopUpdatingLocation()
170
+ self.watchers.remove(at: index)
171
+ }
172
+ if let savedCall = self.bridge?.savedCall(withID: callbackId) {
173
+ self.bridge?.releaseCall(savedCall)
174
+ }
175
+ return call.resolve()
176
+ }
177
+ return call.reject("No callback ID")
178
+ }
179
+ }
180
+
181
+ @objc func openSettings(_ call: CAPPluginCall) {
182
+ DispatchQueue.main.async {
183
+ guard let settingsUrl = URL(
184
+ string: UIApplication.openSettingsURLString
185
+ ) else {
186
+ return call.reject("No link to settings available")
187
+ }
188
+
189
+ if UIApplication.shared.canOpenURL(settingsUrl) {
190
+ UIApplication.shared.open(settingsUrl, completionHandler: {
191
+ (success) in
192
+ if (success) {
193
+ return call.resolve()
194
+ } else {
195
+ return call.reject("Failed to open settings")
196
+ }
197
+ })
198
+ } else {
199
+ return call.reject("Cannot open settings")
200
+ }
201
+ }
202
+ }
203
+
204
+ public func locationManager(
205
+ _ manager: CLLocationManager,
206
+ didFailWithError error: Error
207
+ ) {
208
+ if let watcher = self.watchers.first(
209
+ where: { $0.locationManager == manager }
210
+ ) {
211
+ if let call = self.bridge?.savedCall(withID: watcher.callbackId) {
212
+ if let clErr = error as? CLError {
213
+ if clErr.code == .locationUnknown {
214
+ // This error is sometimes sent by the manager if
215
+ // it cannot get a fix immediately.
216
+ return
217
+ } else if (clErr.code == .denied) {
218
+ watcher.stop()
219
+ return call.reject(
220
+ "Permission denied.",
221
+ "NOT_AUTHORIZED"
222
+ )
223
+ }
224
+ }
225
+ return call.reject(error.localizedDescription, nil, error)
226
+ }
227
+ }
228
+ }
229
+
230
+ public func locationManager(
231
+ _ manager: CLLocationManager,
232
+ didUpdateLocations locations: [CLLocation]
233
+ ) {
234
+ if let location = locations.last {
235
+ if let watcher = self.watchers.first(
236
+ where: { $0.locationManager == manager }
237
+ ) {
238
+ if watcher.isLocationValid(location) {
239
+ if let call = self.bridge?.savedCall(withID: watcher.callbackId) {
240
+ return call.resolve(formatLocation(location))
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ public func locationManager(
248
+ _ manager: CLLocationManager,
249
+ didChangeAuthorization status: CLAuthorizationStatus
250
+ ) {
251
+ // If this method is called before the user decides on a permission, as
252
+ // it is on iOS 14 when the permissions dialog is presented, we ignore
253
+ // it.
254
+ if status != .notDetermined {
255
+ if let watcher = self.watchers.first(
256
+ where: { $0.locationManager == manager }
257
+ ) {
258
+ return watcher.start()
259
+ }
260
+ }
261
+ }
262
+ }
263
+
package/ios/Podfile ADDED
@@ -0,0 +1,16 @@
1
+ platform :ios, '12.0'
2
+
3
+ def capacitor_pods
4
+ # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
5
+ use_frameworks!
6
+ pod 'Capacitor', :path => '../node_modules/@capacitor/ios'
7
+ pod 'CapacitorCordova', :path => '../node_modules/@capacitor/ios'
8
+ end
9
+
10
+ target 'Plugin' do
11
+ capacitor_pods
12
+ end
13
+
14
+ target 'PluginTests' do
15
+ capacitor_pods
16
+ end
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@azatek/background-geolocation",
3
+ "version": "1.0.0",
4
+ "description": "Capacitor plugin for background geolocation tracking with configurable GPS accuracy modes (HIGH/BALANCED/LOW/PASSIVE) for battery optimization. Fork of @capacitor-community/background-geolocation with enhanced accuracy control.",
5
+ "keywords": [
6
+ "capacitor",
7
+ "plugin",
8
+ "geolocation",
9
+ "background",
10
+ "gps",
11
+ "location",
12
+ "tracking",
13
+ "accuracy",
14
+ "battery-optimization",
15
+ "android",
16
+ "ios"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/AzaTek61/background-geolocation.git"
21
+ },
22
+ "author": "AzaTek",
23
+ "license": "MIT",
24
+ "types": "definitions.d.ts",
25
+ "files": [
26
+ "CapacitorCommunityBackgroundGeolocation.podspec",
27
+ "Package.swift",
28
+ "android/build.gradle",
29
+ "android/gradle.properties",
30
+ "android/gradle/wrapper/gradle-wrapper.properties",
31
+ "android/proguard-rules.pro",
32
+ "android/settings.gradle",
33
+ "android/src/main/",
34
+ "definitions.d.ts",
35
+ "ios/Plugin/Info.plist",
36
+ "ios/Plugin/Plugin.*",
37
+ "ios/Plugin/Swift/Plugin.swift",
38
+ "ios/Podfile*"
39
+ ],
40
+ "devDependencies": {
41
+ "@capacitor/android": "^7.0.0",
42
+ "@capacitor/core": "^7.0.0",
43
+ "@capacitor/ios": "^7.0.0"
44
+ },
45
+ "peerDependencies": {
46
+ "@capacitor/core": ">=3.0.0"
47
+ },
48
+ "capacitor": {
49
+ "ios": {
50
+ "src": "ios"
51
+ },
52
+ "android": {
53
+ "src": "android"
54
+ }
55
+ }
56
+ }