@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,289 @@
|
|
|
1
|
+
package dev.bglocation.core.location
|
|
2
|
+
|
|
3
|
+
import dev.bglocation.core.BGLLocationConfig
|
|
4
|
+
import dev.bglocation.core.BGLLocationData
|
|
5
|
+
import dev.bglocation.core.debug.BGLDebugLogger
|
|
6
|
+
import dev.bglocation.core.http.BGLHttpResult
|
|
7
|
+
import dev.bglocation.core.http.BGLHttpSender
|
|
8
|
+
import dev.bglocation.core.http.BGLLocationBuffer
|
|
9
|
+
import dev.bglocation.core.notification.BGLNotificationHelper
|
|
10
|
+
import android.annotation.SuppressLint
|
|
11
|
+
import android.app.Service
|
|
12
|
+
import android.content.Intent
|
|
13
|
+
import android.content.pm.ServiceInfo
|
|
14
|
+
import android.os.Binder
|
|
15
|
+
import android.os.Build
|
|
16
|
+
import android.os.IBinder
|
|
17
|
+
import android.util.Log
|
|
18
|
+
import com.google.android.gms.location.FusedLocationProviderClient
|
|
19
|
+
import com.google.android.gms.location.LocationCallback
|
|
20
|
+
import com.google.android.gms.location.LocationRequest
|
|
21
|
+
import com.google.android.gms.location.LocationResult
|
|
22
|
+
import com.google.android.gms.location.LocationServices
|
|
23
|
+
import com.google.android.gms.location.Priority
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Foreground Service that keeps GPS running in the background.
|
|
27
|
+
*
|
|
28
|
+
* Android 8+ (API 26) requires a Foreground Service with a persistent notification
|
|
29
|
+
* to maintain background execution. Android 14+ also requires `foregroundServiceType="location"`.
|
|
30
|
+
*
|
|
31
|
+
* Dependencies are exposed as `internal var` for testability (Android Service
|
|
32
|
+
* does not support constructor injection).
|
|
33
|
+
*/
|
|
34
|
+
class BGLLocationForegroundService : Service() {
|
|
35
|
+
|
|
36
|
+
companion object {
|
|
37
|
+
private const val TAG = "BGLocation"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
inner class LocalBinder : Binder() {
|
|
41
|
+
fun getService(): BGLLocationForegroundService = this@BGLLocationForegroundService
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private val binder = LocalBinder()
|
|
45
|
+
|
|
46
|
+
// --- Injected dependencies (var for bridge/testing access) ---
|
|
47
|
+
internal var fusedClient: FusedLocationProviderClient? = null
|
|
48
|
+
internal var httpSender: BGLHttpSender? = null
|
|
49
|
+
internal var notificationHelper: BGLNotificationHelper? = null
|
|
50
|
+
internal var heartbeatTimer: BGLHeartbeatTimer? = null
|
|
51
|
+
internal var locationBuffer: BGLLocationBuffer? = null
|
|
52
|
+
var debug: BGLDebugLogger = BGLDebugLogger()
|
|
53
|
+
|
|
54
|
+
private var config = BGLLocationConfig()
|
|
55
|
+
@Volatile
|
|
56
|
+
private var lastLocation: BGLLocationData? = null
|
|
57
|
+
var adaptiveFilter: BGLAdaptiveFilter? = null
|
|
58
|
+
|
|
59
|
+
var isTracking = false
|
|
60
|
+
private set
|
|
61
|
+
|
|
62
|
+
// Callbacks — set by BackgroundLocationPlugin
|
|
63
|
+
var onLocation: ((BGLLocationData) -> Unit)? = null
|
|
64
|
+
var onProviderChange: ((Boolean) -> Unit)? = null
|
|
65
|
+
var onHeartbeat: ((BGLLocationData?) -> Unit)? = null
|
|
66
|
+
var onHttp: ((BGLHttpResult) -> Unit)? = null
|
|
67
|
+
var onMockLocation: ((BGLLocationData) -> Unit)? = null
|
|
68
|
+
@Volatile
|
|
69
|
+
private var mockLocationEmitted = false
|
|
70
|
+
|
|
71
|
+
private val locationCallback = object : LocationCallback() {
|
|
72
|
+
override fun onLocationResult(result: LocationResult) {
|
|
73
|
+
val location = result.lastLocation ?: return
|
|
74
|
+
val data = BGLLocationHelpers.mapLocation(location)
|
|
75
|
+
lastLocation = data
|
|
76
|
+
onLocation?.invoke(data)
|
|
77
|
+
|
|
78
|
+
// C.3: Emit mock location warning once per tracking session
|
|
79
|
+
if (data.isMock && !mockLocationEmitted) {
|
|
80
|
+
mockLocationEmitted = true
|
|
81
|
+
onMockLocation?.invoke(data)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Adaptive distance filter: update EMA and reconfigure if needed
|
|
85
|
+
val filter = adaptiveFilter
|
|
86
|
+
if (filter != null) {
|
|
87
|
+
val shouldReconfigure = filter.update(data.speed)
|
|
88
|
+
if (shouldReconfigure) {
|
|
89
|
+
reconfigureLocationRequest()
|
|
90
|
+
debug.logMessage("Auto distance filter: ${filter.effectiveDistanceFilter.toInt()}m (speed: ${"%.1f".format(data.speed)}m/s)")
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
debug.logLocation(
|
|
95
|
+
lat = data.latitude,
|
|
96
|
+
lng = data.longitude,
|
|
97
|
+
accuracy = data.accuracy,
|
|
98
|
+
speed = data.speed,
|
|
99
|
+
isMoving = data.isMoving
|
|
100
|
+
)
|
|
101
|
+
updateDebugNotification()
|
|
102
|
+
|
|
103
|
+
httpSender?.sendLocation(data) { httpResult ->
|
|
104
|
+
debug.logHttp(
|
|
105
|
+
statusCode = httpResult.statusCode,
|
|
106
|
+
success = httpResult.success,
|
|
107
|
+
error = httpResult.error
|
|
108
|
+
)
|
|
109
|
+
updateDebugNotification()
|
|
110
|
+
onHttp?.invoke(httpResult)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
override fun onCreate() {
|
|
116
|
+
super.onCreate()
|
|
117
|
+
if (fusedClient == null) {
|
|
118
|
+
fusedClient = LocationServices.getFusedLocationProviderClient(this)
|
|
119
|
+
}
|
|
120
|
+
if (httpSender == null) {
|
|
121
|
+
httpSender = BGLHttpSender()
|
|
122
|
+
}
|
|
123
|
+
httpSender?.onFlushProgress = { result ->
|
|
124
|
+
onHttp?.invoke(result)
|
|
125
|
+
}
|
|
126
|
+
if (heartbeatTimer == null) {
|
|
127
|
+
heartbeatTimer = BGLHeartbeatTimer()
|
|
128
|
+
}
|
|
129
|
+
if (notificationHelper == null) {
|
|
130
|
+
notificationHelper = BGLNotificationHelper(this)
|
|
131
|
+
}
|
|
132
|
+
val helper = notificationHelper!!
|
|
133
|
+
helper.createChannel()
|
|
134
|
+
val notification = helper.buildNotification(config.notification)
|
|
135
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
136
|
+
startForeground(BGLNotificationHelper.NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION)
|
|
137
|
+
} else {
|
|
138
|
+
startForeground(BGLNotificationHelper.NOTIFICATION_ID, notification)
|
|
139
|
+
}
|
|
140
|
+
Log.d(TAG, "BGLLocationForegroundService created")
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
override fun onBind(intent: Intent?): IBinder = binder
|
|
144
|
+
|
|
145
|
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
146
|
+
return START_STICKY
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fun configure(newConfig: BGLLocationConfig) {
|
|
150
|
+
config = newConfig
|
|
151
|
+
httpSender?.configure(newConfig.http)
|
|
152
|
+
|
|
153
|
+
// Initialize adaptive filter if in auto mode
|
|
154
|
+
if (newConfig.distanceFilterMode == "auto") {
|
|
155
|
+
val autoConfig = newConfig.autoDistanceFilter ?: BGLAutoDistanceFilterConfig()
|
|
156
|
+
adaptiveFilter = BGLAdaptiveFilter(autoConfig)
|
|
157
|
+
} else {
|
|
158
|
+
adaptiveFilter = null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Set up offline buffer if HTTP + buffer config is provided
|
|
162
|
+
if (newConfig.http != null && newConfig.http.buffer != null) {
|
|
163
|
+
if (locationBuffer == null) {
|
|
164
|
+
locationBuffer = BGLLocationBuffer(this)
|
|
165
|
+
}
|
|
166
|
+
locationBuffer?.configure(newConfig.http.buffer)
|
|
167
|
+
locationBuffer?.onOverflow = { dropped ->
|
|
168
|
+
debug.logMessage("BUFFER_OVERFLOW dropped=$dropped maxSize=${newConfig.http.buffer?.maxSize ?: 1000}")
|
|
169
|
+
}
|
|
170
|
+
httpSender?.setBuffer(locationBuffer)
|
|
171
|
+
} else {
|
|
172
|
+
httpSender?.setBuffer(null)
|
|
173
|
+
locationBuffer?.close()
|
|
174
|
+
locationBuffer = null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
debug.configure(debug = newConfig.debug, debugSounds = newConfig.debugSounds)
|
|
178
|
+
debug.logConfigure(
|
|
179
|
+
distanceFilter = newConfig.distanceFilter,
|
|
180
|
+
heartbeat = newConfig.heartbeatInterval,
|
|
181
|
+
httpUrl = newConfig.http?.url
|
|
182
|
+
)
|
|
183
|
+
notificationHelper?.updateNotification(config.notification)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@SuppressLint("MissingPermission")
|
|
187
|
+
fun startTracking() {
|
|
188
|
+
if (isTracking) return
|
|
189
|
+
mockLocationEmitted = false
|
|
190
|
+
|
|
191
|
+
val initialDistanceFilter = adaptiveFilter?.effectiveDistanceFilter ?: config.distanceFilter
|
|
192
|
+
|
|
193
|
+
val request = buildLocationRequest(initialDistanceFilter)
|
|
194
|
+
|
|
195
|
+
fusedClient?.requestLocationUpdates(request, locationCallback, mainLooper)
|
|
196
|
+
heartbeatTimer?.start(config.heartbeatInterval) {
|
|
197
|
+
onHeartbeat?.invoke(lastLocation)
|
|
198
|
+
debug.logHeartbeat(hasLocation = lastLocation != null)
|
|
199
|
+
updateDebugNotification()
|
|
200
|
+
}
|
|
201
|
+
isTracking = true
|
|
202
|
+
debug.logStart()
|
|
203
|
+
val modeInfo = if (adaptiveFilter != null) "auto (initial=${initialDistanceFilter.toInt()}m)" else "${config.distanceFilter}m"
|
|
204
|
+
Log.d(TAG, "Location tracking started — interval=${config.locationUpdateInterval}ms, distance=$modeInfo")
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fun stopTracking() {
|
|
208
|
+
if (!isTracking) return
|
|
209
|
+
|
|
210
|
+
fusedClient?.removeLocationUpdates(locationCallback)
|
|
211
|
+
heartbeatTimer?.stop()
|
|
212
|
+
adaptiveFilter?.reset()
|
|
213
|
+
isTracking = false
|
|
214
|
+
debug.logStop()
|
|
215
|
+
Log.d(TAG, "Location tracking stopped")
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Reconfigure the FusedLocationProviderClient with a new distance filter.
|
|
220
|
+
* Called by the adaptive filter when the effective distance changes significantly.
|
|
221
|
+
*/
|
|
222
|
+
@SuppressLint("MissingPermission")
|
|
223
|
+
private fun reconfigureLocationRequest() {
|
|
224
|
+
if (!isTracking) return
|
|
225
|
+
val filter = adaptiveFilter ?: return
|
|
226
|
+
|
|
227
|
+
fusedClient?.removeLocationUpdates(locationCallback)
|
|
228
|
+
|
|
229
|
+
val request = buildLocationRequest(filter.effectiveDistanceFilter)
|
|
230
|
+
|
|
231
|
+
fusedClient?.requestLocationUpdates(request, locationCallback, mainLooper)
|
|
232
|
+
Log.d(TAG, "Adaptive filter reconfigured — new distance=${filter.effectiveDistanceFilter.toInt()}m")
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private fun buildLocationRequest(distanceFilter: Float): LocationRequest {
|
|
236
|
+
val priority = when (config.desiredAccuracy) {
|
|
237
|
+
"high" -> Priority.PRIORITY_HIGH_ACCURACY
|
|
238
|
+
"balanced" -> Priority.PRIORITY_BALANCED_POWER_ACCURACY
|
|
239
|
+
"low" -> Priority.PRIORITY_LOW_POWER
|
|
240
|
+
else -> {
|
|
241
|
+
Log.w(TAG, "Unknown desiredAccuracy '${config.desiredAccuracy}', falling back to 'high'")
|
|
242
|
+
Priority.PRIORITY_HIGH_ACCURACY
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return LocationRequest.Builder(priority, config.locationUpdateInterval)
|
|
246
|
+
.setMinUpdateDistanceMeters(distanceFilter)
|
|
247
|
+
.setMinUpdateIntervalMillis(config.fastestLocationUpdateInterval)
|
|
248
|
+
.build()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@SuppressLint("MissingPermission")
|
|
252
|
+
fun getCurrentPosition(callback: (BGLLocationData?) -> Unit) {
|
|
253
|
+
val client = fusedClient
|
|
254
|
+
if (client == null) {
|
|
255
|
+
callback(lastLocation)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
client.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null)
|
|
259
|
+
.addOnSuccessListener { location ->
|
|
260
|
+
if (location != null) {
|
|
261
|
+
callback(BGLLocationHelpers.mapLocation(location))
|
|
262
|
+
} else {
|
|
263
|
+
callback(lastLocation)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
.addOnFailureListener {
|
|
267
|
+
callback(lastLocation)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
override fun onDestroy() {
|
|
272
|
+
stopTracking()
|
|
273
|
+
httpSender?.shutdown()
|
|
274
|
+
debug.release()
|
|
275
|
+
Log.d(TAG, "BGLLocationForegroundService destroyed")
|
|
276
|
+
super.onDestroy()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private fun updateDebugNotification() {
|
|
280
|
+
if (!debug.enabled) return
|
|
281
|
+
|
|
282
|
+
val text = debug.notificationText(
|
|
283
|
+
lat = lastLocation?.latitude,
|
|
284
|
+
lng = lastLocation?.longitude,
|
|
285
|
+
effectiveDistanceFilter = adaptiveFilter?.effectiveDistanceFilter
|
|
286
|
+
)
|
|
287
|
+
notificationHelper?.updateDebugNotification(config.notification, text)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
package dev.bglocation.core.location
|
|
2
|
+
|
|
3
|
+
import android.location.Location
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import dev.bglocation.core.BGLLocationData
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pure helper functions for location data transformation.
|
|
10
|
+
* Centralizes field mapping to avoid duplication across plugin, service, and HTTP layer.
|
|
11
|
+
*/
|
|
12
|
+
object BGLLocationHelpers {
|
|
13
|
+
|
|
14
|
+
/** Speed threshold (m/s) above which the device is considered moving. */
|
|
15
|
+
const val MOTION_SPEED_THRESHOLD = 0.5f
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Map an Android [Location] to a normalized [BGLLocationData].
|
|
19
|
+
*/
|
|
20
|
+
internal fun mapLocation(location: Location): BGLLocationData {
|
|
21
|
+
return BGLLocationData(
|
|
22
|
+
latitude = location.latitude,
|
|
23
|
+
longitude = location.longitude,
|
|
24
|
+
accuracy = location.accuracy,
|
|
25
|
+
speed = maxOf(location.speed, 0f),
|
|
26
|
+
heading = if (location.hasBearing()) location.bearing else -1f,
|
|
27
|
+
altitude = location.altitude,
|
|
28
|
+
timestamp = location.time,
|
|
29
|
+
isMoving = location.speed > MOTION_SPEED_THRESHOLD,
|
|
30
|
+
isMock = isMockLocation(location)
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detect whether a [Location] was generated by a mock/test provider.
|
|
36
|
+
*/
|
|
37
|
+
@Suppress("DEPRECATION")
|
|
38
|
+
internal fun isMockLocation(location: Location): Boolean {
|
|
39
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
40
|
+
location.isMock
|
|
41
|
+
} else {
|
|
42
|
+
location.isFromMockProvider
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert [BGLLocationData] to a [JSONObject] for event emission.
|
|
48
|
+
* Single source of truth for the 9 location fields exposed to JS.
|
|
49
|
+
*/
|
|
50
|
+
fun toJSONObject(location: BGLLocationData): JSONObject {
|
|
51
|
+
return JSONObject().apply {
|
|
52
|
+
put("latitude", location.latitude)
|
|
53
|
+
put("longitude", location.longitude)
|
|
54
|
+
put("accuracy", location.accuracy.toDouble())
|
|
55
|
+
put("speed", location.speed.toDouble())
|
|
56
|
+
put("heading", location.heading.toDouble())
|
|
57
|
+
put("altitude", location.altitude)
|
|
58
|
+
put("timestamp", location.timestamp)
|
|
59
|
+
put("isMoving", location.isMoving)
|
|
60
|
+
put("isMock", location.isMock)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build the HTTP POST body wrapping location in a `{ "location": { ... } }` envelope.
|
|
66
|
+
*/
|
|
67
|
+
internal fun buildLocationBody(location: BGLLocationData): JSONObject {
|
|
68
|
+
return JSONObject().apply {
|
|
69
|
+
put("location", toJSONObject(location))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
package dev.bglocation.core.location
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.pm.PackageManager
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.util.Log
|
|
9
|
+
import androidx.core.app.ActivityCompat
|
|
10
|
+
import org.json.JSONObject
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Handles permission checking and rationale emission logic.
|
|
14
|
+
*/
|
|
15
|
+
class BGLPermissionManager {
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
private const val TAG = "BGLocation"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build permission status from current permission state.
|
|
23
|
+
*/
|
|
24
|
+
fun buildPermissionStatus(
|
|
25
|
+
context: Context,
|
|
26
|
+
activity: Activity?,
|
|
27
|
+
capPermissionState: String?
|
|
28
|
+
): JSONObject {
|
|
29
|
+
val fineGranted = ActivityCompat.checkSelfPermission(
|
|
30
|
+
context, Manifest.permission.ACCESS_FINE_LOCATION
|
|
31
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
32
|
+
|
|
33
|
+
val bgGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
34
|
+
ActivityCompat.checkSelfPermission(
|
|
35
|
+
context, Manifest.permission.ACCESS_BACKGROUND_LOCATION
|
|
36
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
37
|
+
} else {
|
|
38
|
+
fineGranted
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
val locationState = when {
|
|
42
|
+
fineGranted -> "granted"
|
|
43
|
+
activity != null && ActivityCompat.shouldShowRequestPermissionRationale(
|
|
44
|
+
activity, Manifest.permission.ACCESS_FINE_LOCATION
|
|
45
|
+
) -> "prompt"
|
|
46
|
+
else -> {
|
|
47
|
+
if (capPermissionState == "denied") "denied" else "prompt"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
val bgState = when {
|
|
52
|
+
bgGranted -> "granted"
|
|
53
|
+
!fineGranted -> locationState
|
|
54
|
+
else -> "prompt"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return JSONObject().apply {
|
|
58
|
+
put("location", locationState)
|
|
59
|
+
put("backgroundLocation", bgState)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build rationale for background location permission on Android 11+.
|
|
65
|
+
* Returns the rationale data if it should be emitted, or null if not applicable.
|
|
66
|
+
*/
|
|
67
|
+
fun buildBackgroundRationale(
|
|
68
|
+
context: Context,
|
|
69
|
+
activity: Activity?,
|
|
70
|
+
permissions: List<String>
|
|
71
|
+
): JSONObject? {
|
|
72
|
+
val requestingBackground = permissions.contains("backgroundLocation")
|
|
73
|
+
if (!requestingBackground || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return null
|
|
74
|
+
|
|
75
|
+
val fineGranted = ActivityCompat.checkSelfPermission(
|
|
76
|
+
context, Manifest.permission.ACCESS_FINE_LOCATION
|
|
77
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
78
|
+
|
|
79
|
+
val bgGranted = ActivityCompat.checkSelfPermission(
|
|
80
|
+
context, Manifest.permission.ACCESS_BACKGROUND_LOCATION
|
|
81
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
82
|
+
|
|
83
|
+
if (!fineGranted || bgGranted) return null
|
|
84
|
+
|
|
85
|
+
val shouldShow = activity?.let {
|
|
86
|
+
ActivityCompat.shouldShowRequestPermissionRationale(
|
|
87
|
+
it, Manifest.permission.ACCESS_BACKGROUND_LOCATION
|
|
88
|
+
)
|
|
89
|
+
} ?: false
|
|
90
|
+
|
|
91
|
+
Log.d(TAG, "Emitted onPermissionRationale before background location request (shouldShowRationale=$shouldShow)")
|
|
92
|
+
|
|
93
|
+
return JSONObject().apply {
|
|
94
|
+
put("permission", "backgroundLocation")
|
|
95
|
+
put("shouldShowRationale", shouldShow)
|
|
96
|
+
put("message", "Background location permission is required for continuous tracking. On Android 11+, please select \"Allow all the time\" in the app's location settings.")
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
package dev.bglocation.core.notification
|
|
2
|
+
|
|
3
|
+
import dev.bglocation.core.BGLNotificationConfig
|
|
4
|
+
import android.app.Notification
|
|
5
|
+
import android.app.NotificationChannel
|
|
6
|
+
import android.app.NotificationManager
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import androidx.core.app.NotificationCompat
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Manages the foreground service notification for background location tracking.
|
|
12
|
+
*
|
|
13
|
+
* Extracted from BGLLocationForegroundService (SRP) — handles channel creation,
|
|
14
|
+
* notification building, and debug info display.
|
|
15
|
+
*/
|
|
16
|
+
class BGLNotificationHelper(private val context: Context) {
|
|
17
|
+
|
|
18
|
+
companion object {
|
|
19
|
+
const val CHANNEL_ID = "bgl_location_channel"
|
|
20
|
+
const val NOTIFICATION_ID = 1001
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fun createChannel() {
|
|
24
|
+
val channel = NotificationChannel(
|
|
25
|
+
CHANNEL_ID,
|
|
26
|
+
"Location Tracking",
|
|
27
|
+
NotificationManager.IMPORTANCE_LOW
|
|
28
|
+
).apply {
|
|
29
|
+
description = "Background location tracking for tour"
|
|
30
|
+
}
|
|
31
|
+
val manager = context.getSystemService(NotificationManager::class.java)
|
|
32
|
+
manager.createNotificationChannel(channel)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fun buildNotification(config: BGLNotificationConfig): Notification {
|
|
36
|
+
return baseBuilder()
|
|
37
|
+
.setContentTitle(config.title)
|
|
38
|
+
.setContentText(config.text)
|
|
39
|
+
.build()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fun updateNotification(config: BGLNotificationConfig) {
|
|
43
|
+
val manager = context.getSystemService(NotificationManager::class.java) ?: return
|
|
44
|
+
manager.notify(NOTIFICATION_ID, buildNotification(config))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fun updateDebugNotification(config: BGLNotificationConfig, debugText: String) {
|
|
48
|
+
val notification = baseBuilder()
|
|
49
|
+
.setContentTitle("${config.title} [DEBUG]")
|
|
50
|
+
.setContentText(debugText)
|
|
51
|
+
.build()
|
|
52
|
+
val manager = context.getSystemService(NotificationManager::class.java)
|
|
53
|
+
manager.notify(NOTIFICATION_ID, notification)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a valid notification small icon.
|
|
58
|
+
*
|
|
59
|
+
* Adaptive (mipmap) launcher icons cannot be used as notification small icons —
|
|
60
|
+
* they render as blank squares on most Android versions.
|
|
61
|
+
* Try a dedicated drawable first, then fall back to a system icon.
|
|
62
|
+
*/
|
|
63
|
+
internal fun getNotificationIcon(): Int {
|
|
64
|
+
val custom = context.resources.getIdentifier(
|
|
65
|
+
"ic_notification", "drawable", context.packageName
|
|
66
|
+
)
|
|
67
|
+
if (custom != 0) return custom
|
|
68
|
+
return android.R.drawable.ic_menu_mylocation
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private fun baseBuilder(): NotificationCompat.Builder {
|
|
72
|
+
return NotificationCompat.Builder(context, CHANNEL_ID)
|
|
73
|
+
.setSmallIcon(getNotificationIcon())
|
|
74
|
+
.setOngoing(true)
|
|
75
|
+
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
|
76
|
+
}
|
|
77
|
+
}
|