@bglocation/capacitor 1.1.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.
- package/CapacitorBackgroundLocation.podspec +19 -0
- package/LICENSE.md +97 -0
- package/Package.swift +44 -0
- package/README.md +264 -0
- package/android/build.gradle +74 -0
- package/android/src/main/AndroidManifest.xml +37 -0
- package/android/src/main/kotlin/dev/bglocation/BackgroundLocationPlugin.kt +684 -0
- package/android/src/main/kotlin/dev/bglocation/core/Models.kt +76 -0
- package/android/src/main/kotlin/dev/bglocation/core/battery/BGLBatteryHelper.kt +127 -0
- package/android/src/main/kotlin/dev/bglocation/core/boot/BGLBootCompletedReceiver.kt +32 -0
- package/android/src/main/kotlin/dev/bglocation/core/config/BGLConfigParser.kt +114 -0
- package/android/src/main/kotlin/dev/bglocation/core/config/BGLVersion.kt +6 -0
- package/android/src/main/kotlin/dev/bglocation/core/debug/BGLDebugLogger.kt +174 -0
- package/android/src/main/kotlin/dev/bglocation/core/geofence/BGLGeofenceBroadcastReceiver.kt +93 -0
- package/android/src/main/kotlin/dev/bglocation/core/geofence/BGLGeofenceManager.kt +310 -0
- package/android/src/main/kotlin/dev/bglocation/core/http/BGLHttpSender.kt +187 -0
- package/android/src/main/kotlin/dev/bglocation/core/http/BGLLocationBuffer.kt +152 -0
- package/android/src/main/kotlin/dev/bglocation/core/license/BGLBuildConfig.kt +16 -0
- package/android/src/main/kotlin/dev/bglocation/core/license/BGLLicenseEnforcer.kt +137 -0
- package/android/src/main/kotlin/dev/bglocation/core/license/BGLLicenseValidator.kt +134 -0
- package/android/src/main/kotlin/dev/bglocation/core/license/BGLTrialTimer.kt +176 -0
- package/android/src/main/kotlin/dev/bglocation/core/location/BGLAdaptiveFilter.kt +94 -0
- package/android/src/main/kotlin/dev/bglocation/core/location/BGLHeartbeatTimer.kt +38 -0
- package/android/src/main/kotlin/dev/bglocation/core/location/BGLLocationForegroundService.kt +289 -0
- package/android/src/main/kotlin/dev/bglocation/core/location/BGLLocationHelpers.kt +72 -0
- package/android/src/main/kotlin/dev/bglocation/core/location/BGLPermissionManager.kt +99 -0
- package/android/src/main/kotlin/dev/bglocation/core/notification/BGLNotificationHelper.kt +77 -0
- package/dist/esm/definitions.d.ts +390 -0
- package/dist/esm/definitions.js +3 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +26 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +47 -0
- package/dist/esm/web.js +231 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/esm/web.test.d.ts +1 -0
- package/dist/esm/web.test.js +940 -0
- package/dist/esm/web.test.js.map +1 -0
- package/dist/plugin.cjs.js +267 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +270 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/BGLocationCore/Config/BGLConfigParser.swift +88 -0
- package/ios/Sources/BGLocationCore/Config/BGLVersion.swift +6 -0
- package/ios/Sources/BGLocationCore/Debug/BGLDebugLogger.swift +201 -0
- package/ios/Sources/BGLocationCore/Geofence/BGLGeofenceManager.swift +538 -0
- package/ios/Sources/BGLocationCore/Http/BGLHttpSender.swift +227 -0
- package/ios/Sources/BGLocationCore/Http/BGLLocationBuffer.swift +198 -0
- package/ios/Sources/BGLocationCore/License/BGLBuildConfig.swift +11 -0
- package/ios/Sources/BGLocationCore/License/BGLLicenseEnforcer.swift +134 -0
- package/ios/Sources/BGLocationCore/License/BGLLicenseValidator.swift +163 -0
- package/ios/Sources/BGLocationCore/License/BGLTrialTimer.swift +168 -0
- package/ios/Sources/BGLocationCore/Location/BGLAdaptiveFilter.swift +91 -0
- package/ios/Sources/BGLocationCore/Location/BGLHeartbeatTimer.swift +50 -0
- package/ios/Sources/BGLocationCore/Location/BGLLocationData.swift +48 -0
- package/ios/Sources/BGLocationCore/Location/BGLLocationHelpers.swift +42 -0
- package/ios/Sources/BGLocationCore/Location/BGLLocationManager.swift +268 -0
- package/ios/Sources/BGLocationCore/Location/BGLPermissionManager.swift +33 -0
- package/ios/Sources/BackgroundLocationPlugin/BackgroundLocationPlugin.swift +657 -0
- package/package.json +75 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
package dev.bglocation
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.ComponentName
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.content.ServiceConnection
|
|
8
|
+
import android.content.pm.PackageManager
|
|
9
|
+
import android.os.Handler
|
|
10
|
+
import android.os.IBinder
|
|
11
|
+
import android.os.Looper
|
|
12
|
+
import android.util.Log
|
|
13
|
+
import androidx.core.app.ActivityCompat
|
|
14
|
+
import com.getcapacitor.JSObject
|
|
15
|
+
import com.getcapacitor.Plugin
|
|
16
|
+
import com.getcapacitor.PluginCall
|
|
17
|
+
import com.getcapacitor.PluginMethod
|
|
18
|
+
import com.getcapacitor.annotation.CapacitorPlugin
|
|
19
|
+
import com.getcapacitor.annotation.Permission
|
|
20
|
+
import dev.bglocation.core.*
|
|
21
|
+
import dev.bglocation.core.battery.BGLBatteryHelper
|
|
22
|
+
import dev.bglocation.core.battery.BGLBatteryState
|
|
23
|
+
import dev.bglocation.core.config.BGLConfigParser
|
|
24
|
+
import dev.bglocation.core.config.BGLVersion
|
|
25
|
+
import dev.bglocation.core.debug.BGLDebugLogger
|
|
26
|
+
import dev.bglocation.core.geofence.BGLGeofenceBroadcastReceiver
|
|
27
|
+
import dev.bglocation.core.geofence.BGLGeofenceConfig
|
|
28
|
+
import dev.bglocation.core.geofence.BGLGeofenceManager
|
|
29
|
+
import dev.bglocation.core.http.BGLHttpResult
|
|
30
|
+
import dev.bglocation.core.license.BGLBuildConfig
|
|
31
|
+
import dev.bglocation.core.license.BGLLicenseEnforcer
|
|
32
|
+
import dev.bglocation.core.license.BGLLicenseValidator
|
|
33
|
+
import dev.bglocation.core.license.BGLTrialTimer
|
|
34
|
+
import dev.bglocation.core.location.BGLLocationForegroundService
|
|
35
|
+
import dev.bglocation.core.location.BGLLocationHelpers
|
|
36
|
+
import dev.bglocation.core.location.BGLPermissionManager
|
|
37
|
+
import org.json.JSONObject
|
|
38
|
+
|
|
39
|
+
@CapacitorPlugin(
|
|
40
|
+
name = "BackgroundLocation",
|
|
41
|
+
permissions = [
|
|
42
|
+
Permission(
|
|
43
|
+
strings = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION],
|
|
44
|
+
alias = "location"
|
|
45
|
+
),
|
|
46
|
+
Permission(
|
|
47
|
+
strings = ["android.permission.ACCESS_BACKGROUND_LOCATION"],
|
|
48
|
+
alias = "backgroundLocation"
|
|
49
|
+
)
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
class BackgroundLocationPlugin : Plugin() {
|
|
53
|
+
|
|
54
|
+
companion object {
|
|
55
|
+
private const val TAG = "BGLocation"
|
|
56
|
+
private const val LICENSE_PREFS_NAME = "bglocation_license"
|
|
57
|
+
private const val SERVICE_BIND_TIMEOUT_MS = 10_000L
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private val permissionManager = BGLPermissionManager()
|
|
61
|
+
|
|
62
|
+
@PluginMethod
|
|
63
|
+
fun getVersion(call: PluginCall) {
|
|
64
|
+
val result = JSObject()
|
|
65
|
+
result.put("pluginVersion", "1.1.0")
|
|
66
|
+
result.put("coreVersion", BGLVersion.VERSION)
|
|
67
|
+
call.resolve(result)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@PluginMethod
|
|
71
|
+
override fun checkPermissions(call: PluginCall) {
|
|
72
|
+
val capState = getPermissionState("location")
|
|
73
|
+
val capStateStr = if (capState != null && capState.toString() == "denied") "denied" else null
|
|
74
|
+
val result = permissionManager.buildPermissionStatus(context, activity, capStateStr)
|
|
75
|
+
call.resolve(result.toJSObject())
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// C.2: Override requestPermissions to emit onPermissionRationale before
|
|
79
|
+
// requesting background location on Android 11+ (API 30+).
|
|
80
|
+
// On Android 11+, the system dialog does NOT show "Allow all the time" —
|
|
81
|
+
// the user must navigate to Settings manually. We emit an event so the
|
|
82
|
+
// JS layer can display a custom rationale UI before triggering the request.
|
|
83
|
+
@PluginMethod
|
|
84
|
+
override fun requestPermissions(call: PluginCall) {
|
|
85
|
+
val permissions = call.getArray("permissions")?.toList<String>() ?: listOf("location")
|
|
86
|
+
|
|
87
|
+
val rationale = permissionManager.buildBackgroundRationale(context, activity, permissions)
|
|
88
|
+
if (rationale != null) {
|
|
89
|
+
notifyListeners("onPermissionRationale", rationale.toJSObject())
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Delegate to Capacitor's built-in permission handling
|
|
93
|
+
super.requestPermissions(call)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private var locationService: BGLLocationForegroundService? = null
|
|
97
|
+
private var isBound = false
|
|
98
|
+
private var pendingStartCall: PluginCall? = null
|
|
99
|
+
private val startTimeoutHandler = Handler(Looper.getMainLooper())
|
|
100
|
+
|
|
101
|
+
// Configuration stored until service starts
|
|
102
|
+
internal var config = BGLLocationConfig()
|
|
103
|
+
internal lateinit var batteryHelper: BGLBatteryHelper
|
|
104
|
+
|
|
105
|
+
// Licensing
|
|
106
|
+
internal lateinit var licenseEnforcer: BGLLicenseEnforcer
|
|
107
|
+
// Keep direct accessors for backward-compatible test access
|
|
108
|
+
internal val licenseMode: BGLLicenseMode get() = licenseEnforcer.licenseMode
|
|
109
|
+
internal val trialTimer: BGLTrialTimer? get() = licenseEnforcer.trialTimer
|
|
110
|
+
|
|
111
|
+
// Geofencing
|
|
112
|
+
private var geofenceManager: BGLGeofenceManager? = null
|
|
113
|
+
private var isConfigured = false
|
|
114
|
+
|
|
115
|
+
// Debug logger for geofence operations (lives in bridge, not service)
|
|
116
|
+
private val geofenceDebug = BGLDebugLogger()
|
|
117
|
+
|
|
118
|
+
override fun load() {
|
|
119
|
+
BGLBuildConfig.buildEpoch = BuildConfig.PLUGIN_BUILD_EPOCH
|
|
120
|
+
batteryHelper = BGLBatteryHelper(context)
|
|
121
|
+
|
|
122
|
+
val licensePrefs = context.getSharedPreferences(LICENSE_PREFS_NAME, Context.MODE_PRIVATE)
|
|
123
|
+
val licenseValidator = BGLLicenseValidator(licensePrefs)
|
|
124
|
+
licenseEnforcer = BGLLicenseEnforcer(licensePrefs, licenseValidator)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private val serviceConnection = object : ServiceConnection {
|
|
128
|
+
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
|
129
|
+
startTimeoutHandler.removeCallbacksAndMessages(null)
|
|
130
|
+
|
|
131
|
+
val localBinder = binder as BGLLocationForegroundService.LocalBinder
|
|
132
|
+
locationService = localBinder.getService()
|
|
133
|
+
isBound = true
|
|
134
|
+
|
|
135
|
+
locationService?.configure(config)
|
|
136
|
+
|
|
137
|
+
// IMPORTANT: Set callbacks BEFORE startTracking() to prevent
|
|
138
|
+
// race conditions where events fire before handlers are registered.
|
|
139
|
+
locationService?.onLocation = { location ->
|
|
140
|
+
emitLocation(location)
|
|
141
|
+
}
|
|
142
|
+
locationService?.onProviderChange = { enabled ->
|
|
143
|
+
emitProviderChange(enabled)
|
|
144
|
+
}
|
|
145
|
+
locationService?.onHeartbeat = { location ->
|
|
146
|
+
emitHeartbeat(location)
|
|
147
|
+
}
|
|
148
|
+
locationService?.onHttp = { result ->
|
|
149
|
+
emitHttp(result)
|
|
150
|
+
}
|
|
151
|
+
locationService?.debug?.onDebug = { message ->
|
|
152
|
+
emitDebug(message)
|
|
153
|
+
}
|
|
154
|
+
locationService?.onMockLocation = { location ->
|
|
155
|
+
emitMockLocation(location)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
locationService?.startTracking()
|
|
159
|
+
|
|
160
|
+
// Start trial timer if in trial mode
|
|
161
|
+
licenseEnforcer.startTrialIfNeeded()
|
|
162
|
+
|
|
163
|
+
// Emit battery warning if optimization is active (B.1)
|
|
164
|
+
checkAndEmitBatteryWarning()
|
|
165
|
+
|
|
166
|
+
pendingStartCall?.let { call ->
|
|
167
|
+
val result = JSObject().apply {
|
|
168
|
+
put("enabled", true)
|
|
169
|
+
put("tracking", true)
|
|
170
|
+
}
|
|
171
|
+
call.resolve(result)
|
|
172
|
+
pendingStartCall = null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
Log.d(TAG, "BackgroundLocation service connected")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
override fun onServiceDisconnected(name: ComponentName?) {
|
|
179
|
+
locationService = null
|
|
180
|
+
isBound = false
|
|
181
|
+
Log.d(TAG, "BackgroundLocation service disconnected")
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@PluginMethod
|
|
186
|
+
fun configure(call: PluginCall) {
|
|
187
|
+
config = BGLConfigParser.parseLocationConfig(call.data)
|
|
188
|
+
|
|
189
|
+
// --- License validation ---
|
|
190
|
+
val licenseKey = getConfig().getString("licenseKey")
|
|
191
|
+
val bundleId = context.packageName
|
|
192
|
+
val configureResult = JSObject()
|
|
193
|
+
|
|
194
|
+
config = licenseEnforcer.validateLicense(licenseKey, bundleId, config, configureResult) {
|
|
195
|
+
onTrialExpired()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// If service is already running, update config
|
|
199
|
+
locationService?.configure(config)
|
|
200
|
+
|
|
201
|
+
// --- Geofencing setup ---
|
|
202
|
+
val manager = BGLGeofenceManager(context)
|
|
203
|
+
manager.onGeofenceEvent = { event ->
|
|
204
|
+
notifyListeners("onGeofence", event.toJSONObject().toJSObject())
|
|
205
|
+
Log.d(TAG, "Geofence ${event.action}: ${event.identifier}")
|
|
206
|
+
}
|
|
207
|
+
geofenceManager = manager
|
|
208
|
+
BGLGeofenceBroadcastReceiver.geofenceManager = manager
|
|
209
|
+
|
|
210
|
+
// Configure debug logger for geofence operations
|
|
211
|
+
geofenceDebug.configure(config.debug, config.debugSounds)
|
|
212
|
+
geofenceDebug.onDebug = { message -> emitDebug(message) }
|
|
213
|
+
BGLGeofenceBroadcastReceiver.debugLogger = geofenceDebug
|
|
214
|
+
|
|
215
|
+
// Re-register persisted geofences (they survive app restarts but not reboots)
|
|
216
|
+
manager.reRegisterAllGeofences()
|
|
217
|
+
|
|
218
|
+
isConfigured = true
|
|
219
|
+
|
|
220
|
+
Log.d(TAG, "BackgroundLocation configured — distanceFilter=${config.distanceFilter} (mode=${config.distanceFilterMode}), heartbeat=${config.heartbeatInterval}s, http=${config.http?.url ?: "disabled"}")
|
|
221
|
+
configureResult.put("distanceFilterMode", config.distanceFilterMode)
|
|
222
|
+
call.resolve(configureResult)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@PluginMethod
|
|
226
|
+
fun start(call: PluginCall) {
|
|
227
|
+
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
|
|
228
|
+
!= PackageManager.PERMISSION_GRANTED
|
|
229
|
+
) {
|
|
230
|
+
call.reject("Location permission not granted")
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Trial mode: check cooldown before allowing start
|
|
235
|
+
val cooldown = licenseEnforcer.checkCooldown()
|
|
236
|
+
if (cooldown != null) {
|
|
237
|
+
call.reject(
|
|
238
|
+
cooldown.message,
|
|
239
|
+
"TRIAL_COOLDOWN",
|
|
240
|
+
Exception("Trial cooldown active"),
|
|
241
|
+
JSObject().apply {
|
|
242
|
+
put("remainingSeconds", cooldown.remainingSeconds)
|
|
243
|
+
put("message", cooldown.message)
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Reject any previous unresolved start() call
|
|
250
|
+
pendingStartCall?.reject("Superseded by a new start() call")
|
|
251
|
+
pendingStartCall = call
|
|
252
|
+
|
|
253
|
+
// Add timeout for service binding — reject if onServiceConnected not called within 10s
|
|
254
|
+
startTimeoutHandler.removeCallbacksAndMessages(null)
|
|
255
|
+
startTimeoutHandler.postDelayed({
|
|
256
|
+
pendingStartCall?.reject("Service connection timed out")
|
|
257
|
+
pendingStartCall = null
|
|
258
|
+
}, SERVICE_BIND_TIMEOUT_MS)
|
|
259
|
+
|
|
260
|
+
val intent = Intent(context, BGLLocationForegroundService::class.java)
|
|
261
|
+
context.startForegroundService(intent)
|
|
262
|
+
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
|
263
|
+
|
|
264
|
+
Log.d(TAG, "BackgroundLocation starting service")
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Internal stop — tears down service, unbinds, and clears references.
|
|
269
|
+
* Shared by stop() and onTrialExpired() to avoid duplicating cleanup logic.
|
|
270
|
+
*/
|
|
271
|
+
private fun performStopTracking() {
|
|
272
|
+
locationService?.stopTracking()
|
|
273
|
+
if (isBound) {
|
|
274
|
+
context.unbindService(serviceConnection)
|
|
275
|
+
isBound = false
|
|
276
|
+
}
|
|
277
|
+
val intent = Intent(context, BGLLocationForegroundService::class.java)
|
|
278
|
+
context.stopService(intent)
|
|
279
|
+
locationService = null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@PluginMethod
|
|
283
|
+
fun stop(call: PluginCall) {
|
|
284
|
+
// Only stop the trial timer if no geofences are active
|
|
285
|
+
licenseEnforcer.stopTrialIfNoGeofences(geofenceManager?.hasGeofences == true)
|
|
286
|
+
performStopTracking()
|
|
287
|
+
|
|
288
|
+
val hasPermission = ActivityCompat.checkSelfPermission(
|
|
289
|
+
context, Manifest.permission.ACCESS_FINE_LOCATION
|
|
290
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
291
|
+
|
|
292
|
+
Log.d(TAG, "BackgroundLocation stopped")
|
|
293
|
+
val result = JSObject().apply {
|
|
294
|
+
put("enabled", hasPermission)
|
|
295
|
+
put("tracking", false)
|
|
296
|
+
}
|
|
297
|
+
call.resolve(result)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
@PluginMethod
|
|
301
|
+
fun getState(call: PluginCall) {
|
|
302
|
+
val isTracking = locationService?.isTracking ?: false
|
|
303
|
+
val hasPermission = activity?.let {
|
|
304
|
+
ActivityCompat.checkSelfPermission(it, Manifest.permission.ACCESS_FINE_LOCATION) ==
|
|
305
|
+
PackageManager.PERMISSION_GRANTED
|
|
306
|
+
} ?: false
|
|
307
|
+
val result = JSObject().apply {
|
|
308
|
+
put("enabled", hasPermission)
|
|
309
|
+
put("tracking", isTracking)
|
|
310
|
+
}
|
|
311
|
+
call.resolve(result)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
@PluginMethod
|
|
315
|
+
fun getCurrentPosition(call: PluginCall) {
|
|
316
|
+
val service = locationService
|
|
317
|
+
if (service == null) {
|
|
318
|
+
call.reject("Service not running")
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
service.getCurrentPosition { location ->
|
|
323
|
+
if (location != null) {
|
|
324
|
+
call.resolve(BGLLocationHelpers.toJSONObject(location).toJSObject())
|
|
325
|
+
} else {
|
|
326
|
+
call.reject("Unable to get current position — no location available")
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// MARK: - Geofencing Methods
|
|
332
|
+
|
|
333
|
+
@PluginMethod
|
|
334
|
+
fun addGeofence(call: PluginCall) {
|
|
335
|
+
val manager = geofenceManager
|
|
336
|
+
if (!isConfigured || manager == null) {
|
|
337
|
+
call.reject("Plugin not configured. Call configure() first.", "NOT_CONFIGURED")
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
val cooldown = licenseEnforcer.checkCooldown()
|
|
342
|
+
if (cooldown != null) {
|
|
343
|
+
call.reject(
|
|
344
|
+
cooldown.message,
|
|
345
|
+
"TRIAL_COOLDOWN",
|
|
346
|
+
Exception("Trial cooldown active"),
|
|
347
|
+
JSObject().apply { put("remainingSeconds", cooldown.remainingSeconds) }
|
|
348
|
+
)
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
val identifier = call.getString("identifier")
|
|
353
|
+
if (identifier == null) {
|
|
354
|
+
call.reject("Missing required parameter: identifier")
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
val latitude = call.getDouble("latitude")
|
|
358
|
+
val longitude = call.getDouble("longitude")
|
|
359
|
+
val radius = call.getDouble("radius")
|
|
360
|
+
|
|
361
|
+
if (latitude == null || longitude == null || radius == null) {
|
|
362
|
+
call.reject("Missing required parameters: latitude, longitude, radius")
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
val extras = call.getObject("extras", null)?.let { extrasObj ->
|
|
367
|
+
extrasObj.keys().asSequence().associateWith { key -> extrasObj.optString(key, "") }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
val geofence = BGLGeofenceConfig(
|
|
371
|
+
identifier = identifier,
|
|
372
|
+
latitude = latitude,
|
|
373
|
+
longitude = longitude,
|
|
374
|
+
radius = radius,
|
|
375
|
+
notifyOnEntry = call.getBoolean("notifyOnEntry", true) ?: true,
|
|
376
|
+
notifyOnExit = call.getBoolean("notifyOnExit", true) ?: true,
|
|
377
|
+
notifyOnDwell = call.getBoolean("notifyOnDwell", false) ?: false,
|
|
378
|
+
dwellDelay = call.getInt("dwellDelay", 300) ?: 300,
|
|
379
|
+
extras = extras
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
manager.addGeofence(geofence,
|
|
383
|
+
onSuccess = {
|
|
384
|
+
licenseEnforcer.startTrialIfNeeded()
|
|
385
|
+
geofenceDebug.logGeofenceAdd(identifier)
|
|
386
|
+
Log.d(TAG, "Geofence added: $identifier")
|
|
387
|
+
call.resolve()
|
|
388
|
+
},
|
|
389
|
+
onError = { errorMsg ->
|
|
390
|
+
val code = if (errorMsg.contains("limit")) "GEOFENCE_LIMIT_EXCEEDED" else "GEOFENCE_ERROR"
|
|
391
|
+
call.reject(errorMsg, code)
|
|
392
|
+
}
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
@PluginMethod
|
|
397
|
+
fun addGeofences(call: PluginCall) {
|
|
398
|
+
val manager = geofenceManager
|
|
399
|
+
if (!isConfigured || manager == null) {
|
|
400
|
+
call.reject("Plugin not configured. Call configure() first.", "NOT_CONFIGURED")
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
val cooldown = licenseEnforcer.checkCooldown()
|
|
405
|
+
if (cooldown != null) {
|
|
406
|
+
call.reject(
|
|
407
|
+
cooldown.message,
|
|
408
|
+
"TRIAL_COOLDOWN",
|
|
409
|
+
Exception("Trial cooldown active"),
|
|
410
|
+
JSObject().apply { put("remainingSeconds", cooldown.remainingSeconds) }
|
|
411
|
+
)
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
val geofencesArray = call.getArray("geofences")
|
|
416
|
+
if (geofencesArray == null) {
|
|
417
|
+
call.reject("Missing required parameter: geofences")
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
val geofences = mutableListOf<BGLGeofenceConfig>()
|
|
422
|
+
for (i in 0 until geofencesArray.length()) {
|
|
423
|
+
val obj = geofencesArray.optJSONObject(i)
|
|
424
|
+
if (obj == null) {
|
|
425
|
+
call.reject("Geofence at index $i is not a valid object")
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
val identifier = if (obj.has("identifier")) obj.getString("identifier") else null
|
|
429
|
+
val lat = if (obj.has("latitude")) obj.getDouble("latitude") else null
|
|
430
|
+
val lng = if (obj.has("longitude")) obj.getDouble("longitude") else null
|
|
431
|
+
val rad = if (obj.has("radius")) obj.getDouble("radius") else null
|
|
432
|
+
|
|
433
|
+
if (identifier == null || lat == null || lng == null || rad == null) {
|
|
434
|
+
call.reject("Geofence at index $i missing required fields (identifier, latitude, longitude, radius)")
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
val extras = obj.optJSONObject("extras")?.let { extrasObj ->
|
|
439
|
+
extrasObj.keys().asSequence().associateWith { key -> extrasObj.optString(key, "") }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
geofences.add(BGLGeofenceConfig(
|
|
443
|
+
identifier = identifier,
|
|
444
|
+
latitude = lat,
|
|
445
|
+
longitude = lng,
|
|
446
|
+
radius = rad,
|
|
447
|
+
notifyOnEntry = obj.optBoolean("notifyOnEntry", true),
|
|
448
|
+
notifyOnExit = obj.optBoolean("notifyOnExit", true),
|
|
449
|
+
notifyOnDwell = obj.optBoolean("notifyOnDwell", false),
|
|
450
|
+
dwellDelay = obj.optInt("dwellDelay", 300),
|
|
451
|
+
extras = extras
|
|
452
|
+
))
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
manager.addGeofences(geofences,
|
|
456
|
+
onSuccess = {
|
|
457
|
+
licenseEnforcer.startTrialIfNeeded()
|
|
458
|
+
geofenceDebug.logGeofenceAddBatch(geofences.size)
|
|
459
|
+
Log.d(TAG, "Batch added ${geofences.size} geofences")
|
|
460
|
+
call.resolve()
|
|
461
|
+
},
|
|
462
|
+
onError = { errorMsg ->
|
|
463
|
+
val code = if (errorMsg.contains("limit")) "GEOFENCE_LIMIT_EXCEEDED" else "GEOFENCE_ERROR"
|
|
464
|
+
call.reject(errorMsg, code)
|
|
465
|
+
}
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
@PluginMethod
|
|
470
|
+
fun removeGeofence(call: PluginCall) {
|
|
471
|
+
val manager = geofenceManager
|
|
472
|
+
if (!isConfigured || manager == null) {
|
|
473
|
+
call.reject("Plugin not configured. Call configure() first.", "NOT_CONFIGURED")
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
val identifier = call.getString("identifier")
|
|
478
|
+
if (identifier == null) {
|
|
479
|
+
call.reject("Missing required parameter: identifier")
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
manager.removeGeofence(identifier)
|
|
484
|
+
geofenceDebug.logGeofenceRemove(identifier)
|
|
485
|
+
|
|
486
|
+
// If no geofences and no tracking, stop trial timer
|
|
487
|
+
val isTracking = locationService?.isTracking ?: false
|
|
488
|
+
licenseEnforcer.stopTrialIfIdle(!manager.hasGeofences, isTracking)
|
|
489
|
+
|
|
490
|
+
Log.d(TAG, "Geofence removed: $identifier")
|
|
491
|
+
call.resolve()
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
@PluginMethod
|
|
495
|
+
fun removeAllGeofences(call: PluginCall) {
|
|
496
|
+
val manager = geofenceManager
|
|
497
|
+
if (!isConfigured || manager == null) {
|
|
498
|
+
call.reject("Plugin not configured. Call configure() first.", "NOT_CONFIGURED")
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
val count = manager.getGeofences().size
|
|
503
|
+
manager.removeAllGeofences()
|
|
504
|
+
geofenceDebug.logGeofenceRemoveAll(count)
|
|
505
|
+
|
|
506
|
+
// If not tracking, stop trial timer
|
|
507
|
+
val isTracking = locationService?.isTracking ?: false
|
|
508
|
+
licenseEnforcer.stopTrialIfIdle(hasGeofences = false, isTracking = isTracking)
|
|
509
|
+
|
|
510
|
+
Log.d(TAG, "All geofences removed")
|
|
511
|
+
call.resolve()
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
@PluginMethod
|
|
515
|
+
fun getGeofences(call: PluginCall) {
|
|
516
|
+
val manager = geofenceManager
|
|
517
|
+
if (!isConfigured || manager == null) {
|
|
518
|
+
call.reject("Plugin not configured. Call configure() first.", "NOT_CONFIGURED")
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
val geofences = manager.getGeofences()
|
|
523
|
+
val result = com.getcapacitor.JSArray()
|
|
524
|
+
for (geofence in geofences) {
|
|
525
|
+
val obj = JSObject().apply {
|
|
526
|
+
put("identifier", geofence.identifier)
|
|
527
|
+
put("latitude", geofence.latitude)
|
|
528
|
+
put("longitude", geofence.longitude)
|
|
529
|
+
put("radius", geofence.radius)
|
|
530
|
+
put("notifyOnEntry", geofence.notifyOnEntry)
|
|
531
|
+
put("notifyOnExit", geofence.notifyOnExit)
|
|
532
|
+
put("notifyOnDwell", geofence.notifyOnDwell)
|
|
533
|
+
put("dwellDelay", geofence.dwellDelay)
|
|
534
|
+
geofence.extras?.let { extras ->
|
|
535
|
+
put("extras", org.json.JSONObject(extras))
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
result.put(obj)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
val response = JSObject().apply {
|
|
542
|
+
put("geofences", result)
|
|
543
|
+
}
|
|
544
|
+
call.resolve(response)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// MARK: - Battery Optimization (B.1)
|
|
548
|
+
|
|
549
|
+
@PluginMethod
|
|
550
|
+
fun checkBatteryOptimization(call: PluginCall) {
|
|
551
|
+
call.resolve(batteryStateToJSObject(batteryHelper.checkState()))
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
@PluginMethod
|
|
555
|
+
fun requestBatteryOptimization(call: PluginCall) {
|
|
556
|
+
val intent = batteryHelper.createBatteryOptimizationIntent()
|
|
557
|
+
if (intent != null) {
|
|
558
|
+
try {
|
|
559
|
+
context.startActivity(intent)
|
|
560
|
+
} catch (e: Exception) {
|
|
561
|
+
Log.w(TAG, "Failed to open battery optimization settings: ${e.message}")
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Return state before user interaction — caller should re-check
|
|
565
|
+
// with checkBatteryOptimization() after app resumes.
|
|
566
|
+
call.resolve(batteryStateToJSObject(batteryHelper.checkState()))
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Emit onBatteryWarning if battery optimization is active.
|
|
571
|
+
* Called automatically on start() to warn about potential tracking issues.
|
|
572
|
+
*/
|
|
573
|
+
private fun checkAndEmitBatteryWarning() {
|
|
574
|
+
val state = batteryHelper.checkState()
|
|
575
|
+
if (!state.isIgnoringOptimizations) {
|
|
576
|
+
notifyListeners("onBatteryWarning", batteryStateToJSObject(state))
|
|
577
|
+
Log.w(TAG, "Battery optimization active — ${state.message}")
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
internal fun batteryStateToJSObject(state: BGLBatteryState): JSObject {
|
|
582
|
+
return JSObject().apply {
|
|
583
|
+
put("isIgnoringOptimizations", state.isIgnoringOptimizations)
|
|
584
|
+
put("manufacturer", state.manufacturer)
|
|
585
|
+
put("helpUrl", state.helpUrl)
|
|
586
|
+
put("message", state.message)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// MARK: - Event emission
|
|
591
|
+
|
|
592
|
+
private fun emitLocation(location: BGLLocationData) {
|
|
593
|
+
val jsObj = BGLLocationHelpers.toJSONObject(location).toJSObject()
|
|
594
|
+
// Add effectiveDistanceFilter in auto mode
|
|
595
|
+
if (config.distanceFilterMode == "auto") {
|
|
596
|
+
val effective = locationService?.adaptiveFilter?.effectiveDistanceFilter
|
|
597
|
+
if (effective != null) {
|
|
598
|
+
jsObj.put("effectiveDistanceFilter", effective.toDouble())
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
notifyListeners("onLocation", jsObj)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private fun emitHeartbeat(location: BGLLocationData?) {
|
|
605
|
+
val data = JSObject().apply {
|
|
606
|
+
if (location != null) {
|
|
607
|
+
put("location", BGLLocationHelpers.toJSONObject(location))
|
|
608
|
+
} else {
|
|
609
|
+
put("location", org.json.JSONObject.NULL)
|
|
610
|
+
}
|
|
611
|
+
put("timestamp", System.currentTimeMillis())
|
|
612
|
+
}
|
|
613
|
+
notifyListeners("onHeartbeat", data)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private fun emitProviderChange(enabled: Boolean) {
|
|
617
|
+
val data = JSObject().apply {
|
|
618
|
+
put("gps", enabled)
|
|
619
|
+
put("network", enabled)
|
|
620
|
+
put("enabled", enabled)
|
|
621
|
+
put("status", if (enabled) 1 else 0)
|
|
622
|
+
}
|
|
623
|
+
notifyListeners("onProviderChange", data)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private fun emitHttp(result: BGLHttpResult) {
|
|
627
|
+
val data = JSObject().apply {
|
|
628
|
+
put("statusCode", result.statusCode)
|
|
629
|
+
put("success", result.success)
|
|
630
|
+
put("responseText", result.responseText)
|
|
631
|
+
if (result.error != null) {
|
|
632
|
+
put("error", result.error)
|
|
633
|
+
}
|
|
634
|
+
put("bufferedCount", result.bufferedCount)
|
|
635
|
+
}
|
|
636
|
+
notifyListeners("onHttp", data)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private fun emitMockLocation(location: BGLLocationData) {
|
|
640
|
+
val data = JSObject().apply {
|
|
641
|
+
put("location", BGLLocationHelpers.toJSONObject(location))
|
|
642
|
+
put("message", "Mock location detected — the device is using a mock/test location provider.")
|
|
643
|
+
}
|
|
644
|
+
notifyListeners("onMockLocation", data)
|
|
645
|
+
Log.w(TAG, "Mock location detected — (${location.latitude}, ${location.longitude})")
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private fun emitDebug(message: String) {
|
|
649
|
+
val data = JSObject().apply {
|
|
650
|
+
put("message", message)
|
|
651
|
+
put("timestamp", System.currentTimeMillis())
|
|
652
|
+
}
|
|
653
|
+
notifyListeners("onDebug", data)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Called by BGLTrialTimer when the 30-minute trial period expires.
|
|
658
|
+
* Auto-stops tracking and emits onTrialExpired event.
|
|
659
|
+
*/
|
|
660
|
+
private fun onTrialExpired() {
|
|
661
|
+
performStopTracking()
|
|
662
|
+
geofenceManager?.removeAllGeofences()
|
|
663
|
+
|
|
664
|
+
val cooldownMinutes = BGLTrialTimer.COOLDOWN_DURATION_SECONDS / 60
|
|
665
|
+
val data = JSObject().apply {
|
|
666
|
+
put("elapsed", BGLTrialTimer.TRIAL_DURATION_SECONDS)
|
|
667
|
+
put("cooldownSeconds", BGLTrialTimer.COOLDOWN_DURATION_SECONDS)
|
|
668
|
+
put("message", "Trial expired. Next start available in $cooldownMinutes minutes. Purchase a license for unlimited use.")
|
|
669
|
+
}
|
|
670
|
+
notifyListeners("onTrialExpired", data)
|
|
671
|
+
Log.w(TAG, "Trial expired — tracking auto-stopped, cooldown started (${BGLTrialTimer.COOLDOWN_DURATION_SECONDS}s)")
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
override fun handleOnDestroy() {
|
|
675
|
+
if (isBound) {
|
|
676
|
+
context.unbindService(serviceConnection)
|
|
677
|
+
isBound = false
|
|
678
|
+
}
|
|
679
|
+
super.handleOnDestroy()
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Convert core JSONObject to Capacitor JSObject */
|
|
684
|
+
private fun JSONObject.toJSObject(): JSObject = JSObject(this.toString())
|