@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,76 @@
|
|
|
1
|
+
package dev.bglocation.core
|
|
2
|
+
|
|
3
|
+
import dev.bglocation.core.location.BGLAutoDistanceFilterConfig
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* License mode for the plugin.
|
|
7
|
+
*/
|
|
8
|
+
enum class BGLLicenseMode {
|
|
9
|
+
FULL,
|
|
10
|
+
TRIAL
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Result of license key validation.
|
|
15
|
+
*/
|
|
16
|
+
sealed class BGLLicenseResult {
|
|
17
|
+
data class Valid(val updatesUntil: Long?) : BGLLicenseResult()
|
|
18
|
+
data class Invalid(val reason: String) : BGLLicenseResult()
|
|
19
|
+
data class UpdatesExpired(val updatesUntil: Long) : BGLLicenseResult()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* HTTP endpoint configuration for native location uploads.
|
|
24
|
+
*/
|
|
25
|
+
data class BGLHttpConfig(
|
|
26
|
+
val url: String,
|
|
27
|
+
val headers: Map<String, String> = emptyMap(),
|
|
28
|
+
val buffer: BGLLocationBufferConfig? = null
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Offline buffer configuration for failed HTTP requests.
|
|
33
|
+
*/
|
|
34
|
+
data class BGLLocationBufferConfig(
|
|
35
|
+
val maxSize: Int = 1000
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Foreground service notification configuration.
|
|
40
|
+
*/
|
|
41
|
+
data class BGLNotificationConfig(
|
|
42
|
+
val title: String = "Background Location",
|
|
43
|
+
val text: String = "Tracking your location"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Configuration for the background location plugin.
|
|
48
|
+
*/
|
|
49
|
+
data class BGLLocationConfig(
|
|
50
|
+
val distanceFilter: Float = 15f,
|
|
51
|
+
val distanceFilterMode: String = "fixed",
|
|
52
|
+
val autoDistanceFilter: BGLAutoDistanceFilterConfig? = null,
|
|
53
|
+
val desiredAccuracy: String = "high",
|
|
54
|
+
val locationUpdateInterval: Long = 5000L,
|
|
55
|
+
val fastestLocationUpdateInterval: Long = 2000L,
|
|
56
|
+
val heartbeatInterval: Int = 15,
|
|
57
|
+
val http: BGLHttpConfig? = null,
|
|
58
|
+
val notification: BGLNotificationConfig = BGLNotificationConfig(),
|
|
59
|
+
val debug: Boolean = false,
|
|
60
|
+
val debugSounds: Boolean = false
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Normalized location data emitted to JS.
|
|
65
|
+
*/
|
|
66
|
+
data class BGLLocationData(
|
|
67
|
+
val latitude: Double,
|
|
68
|
+
val longitude: Double,
|
|
69
|
+
val accuracy: Float,
|
|
70
|
+
val speed: Float,
|
|
71
|
+
val heading: Float,
|
|
72
|
+
val altitude: Double,
|
|
73
|
+
val timestamp: Long,
|
|
74
|
+
val isMoving: Boolean,
|
|
75
|
+
val isMock: Boolean = false
|
|
76
|
+
)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
package dev.bglocation.core.battery
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.net.Uri
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import android.os.PowerManager
|
|
8
|
+
import android.provider.Settings
|
|
9
|
+
import android.util.Log
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper for detecting and handling OEM battery optimization that may kill background tracking.
|
|
13
|
+
*
|
|
14
|
+
* Many Android OEMs (Xiaomi, Huawei, Samsung, OnePlus, Oppo, Vivo) implement aggressive
|
|
15
|
+
* battery optimization beyond stock Android Doze mode. This helper:
|
|
16
|
+
* 1. Checks if the app is exempt from battery optimization
|
|
17
|
+
* 2. Detects the device manufacturer to provide OEM-specific guidance
|
|
18
|
+
* 3. Provides dontkillmyapp.com URLs with per-OEM instructions
|
|
19
|
+
*/
|
|
20
|
+
class BGLBatteryHelper(
|
|
21
|
+
private val context: Context,
|
|
22
|
+
private val manufacturer: String = Build.MANUFACTURER.lowercase()
|
|
23
|
+
) {
|
|
24
|
+
|
|
25
|
+
companion object {
|
|
26
|
+
private const val TAG = "BGLocation"
|
|
27
|
+
private const val BASE_URL = "https://dontkillmyapp.com"
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Mapping of manufacturer names (lowercase) to dontkillmyapp.com slugs.
|
|
31
|
+
* Only OEMs with known aggressive battery management are listed.
|
|
32
|
+
*/
|
|
33
|
+
private val OEM_SLUGS = mapOf(
|
|
34
|
+
"xiaomi" to "xiaomi",
|
|
35
|
+
"huawei" to "huawei",
|
|
36
|
+
"samsung" to "samsung",
|
|
37
|
+
"oneplus" to "oneplus",
|
|
38
|
+
"oppo" to "oppo",
|
|
39
|
+
"vivo" to "vivo",
|
|
40
|
+
"realme" to "realme",
|
|
41
|
+
"meizu" to "meizu",
|
|
42
|
+
"asus" to "asus",
|
|
43
|
+
"lenovo" to "lenovo",
|
|
44
|
+
"nokia" to "nokia",
|
|
45
|
+
"sony" to "sony",
|
|
46
|
+
"wiko" to "wiko"
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Returns the device manufacturer in lowercase.
|
|
52
|
+
*/
|
|
53
|
+
fun getManufacturer(): String = manufacturer
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if the app is currently exempt from battery optimization.
|
|
57
|
+
* When true, Android Doze and App Standby will not restrict the app's background execution.
|
|
58
|
+
*/
|
|
59
|
+
fun isIgnoringBatteryOptimizations(): Boolean {
|
|
60
|
+
val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager
|
|
61
|
+
?: return false
|
|
62
|
+
return pm.isIgnoringBatteryOptimizations(context.packageName)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the dontkillmyapp.com URL for the current device manufacturer.
|
|
67
|
+
* Returns the generic page if the manufacturer is not in the known OEM list.
|
|
68
|
+
*/
|
|
69
|
+
fun getHelpUrl(): String {
|
|
70
|
+
val manufacturer = getManufacturer()
|
|
71
|
+
val slug = OEM_SLUGS[manufacturer]
|
|
72
|
+
return if (slug != null) "$BASE_URL/$slug" else BASE_URL
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether this device's manufacturer is known to have aggressive battery management.
|
|
77
|
+
*/
|
|
78
|
+
fun isAggressiveOem(): Boolean {
|
|
79
|
+
return OEM_SLUGS.containsKey(getManufacturer())
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build a BatteryWarningEvent-compatible map with current state.
|
|
84
|
+
*/
|
|
85
|
+
fun checkState(): BGLBatteryState {
|
|
86
|
+
val isIgnoring = isIgnoringBatteryOptimizations()
|
|
87
|
+
val manufacturer = getManufacturer()
|
|
88
|
+
val helpUrl = getHelpUrl()
|
|
89
|
+
val isAggressive = isAggressiveOem()
|
|
90
|
+
|
|
91
|
+
val message = when {
|
|
92
|
+
isIgnoring -> "Battery optimization is disabled for this app. Background tracking is safe."
|
|
93
|
+
isAggressive -> "Battery optimization is active on $manufacturer. Background tracking may be killed. Please follow the instructions at $helpUrl to whitelist this app."
|
|
94
|
+
else -> "Battery optimization is active. Background tracking may be restricted. Consider disabling battery optimization for this app."
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return BGLBatteryState(
|
|
98
|
+
isIgnoringOptimizations = isIgnoring,
|
|
99
|
+
manufacturer = manufacturer,
|
|
100
|
+
helpUrl = helpUrl,
|
|
101
|
+
message = message
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create an intent to request battery optimization exemption via system dialog.
|
|
107
|
+
* Returns null if the permission is already granted.
|
|
108
|
+
*/
|
|
109
|
+
fun createBatteryOptimizationIntent(): Intent? {
|
|
110
|
+
if (isIgnoringBatteryOptimizations()) return null
|
|
111
|
+
|
|
112
|
+
return Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
|
113
|
+
data = Uri.parse("package:${context.packageName}")
|
|
114
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Represents the current battery optimization state.
|
|
121
|
+
*/
|
|
122
|
+
data class BGLBatteryState(
|
|
123
|
+
val isIgnoringOptimizations: Boolean,
|
|
124
|
+
val manufacturer: String,
|
|
125
|
+
val helpUrl: String,
|
|
126
|
+
val message: String
|
|
127
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package dev.bglocation.core.boot
|
|
2
|
+
|
|
3
|
+
import dev.bglocation.core.geofence.BGLGeofenceManager
|
|
4
|
+
import android.content.BroadcastReceiver
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.util.Log
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Re-registers persisted geofences after device boot.
|
|
11
|
+
*
|
|
12
|
+
* Android clears all registered geofences on reboot. This receiver fires on
|
|
13
|
+
* BOOT_COMPLETED and re-registers them from SharedPreferences persistence.
|
|
14
|
+
*
|
|
15
|
+
* Requires `<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />`
|
|
16
|
+
* in AndroidManifest.xml.
|
|
17
|
+
*/
|
|
18
|
+
class BGLBootCompletedReceiver : BroadcastReceiver() {
|
|
19
|
+
|
|
20
|
+
companion object {
|
|
21
|
+
private const val TAG = "BGLocation"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
25
|
+
if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
|
|
26
|
+
|
|
27
|
+
Log.d(TAG, "BGLBootCompletedReceiver: device booted — re-registering geofences")
|
|
28
|
+
|
|
29
|
+
val manager = BGLGeofenceManager(context)
|
|
30
|
+
manager.reRegisterAllGeofences()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
package dev.bglocation.core.config
|
|
2
|
+
|
|
3
|
+
import dev.bglocation.core.location.BGLAutoDistanceFilterConfig
|
|
4
|
+
import dev.bglocation.core.BGLHttpConfig
|
|
5
|
+
import dev.bglocation.core.BGLLocationBufferConfig
|
|
6
|
+
import dev.bglocation.core.BGLLocationConfig
|
|
7
|
+
import dev.bglocation.core.BGLNotificationConfig
|
|
8
|
+
import org.json.JSONObject
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Framework-agnostic parser: accepts [JSONObject] instead of Capacitor PluginCall.
|
|
12
|
+
*/
|
|
13
|
+
object BGLConfigParser {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse HTTP config from the "http" object.
|
|
17
|
+
* Returns null if no valid URL is provided.
|
|
18
|
+
*/
|
|
19
|
+
fun parseHttpConfig(options: JSONObject): BGLHttpConfig? {
|
|
20
|
+
val httpObj = options.optJSONObject("http") ?: return null
|
|
21
|
+
if (!httpObj.has("url")) return null
|
|
22
|
+
|
|
23
|
+
val headers = mutableMapOf<String, String>()
|
|
24
|
+
httpObj.optJSONObject("headers")?.let { headersObj ->
|
|
25
|
+
headersObj.keys().forEach { key ->
|
|
26
|
+
headers[key] = headersObj.getString(key)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
val bufferObj = httpObj.optJSONObject("buffer")
|
|
30
|
+
val bufferConfig = if (bufferObj != null) {
|
|
31
|
+
BGLLocationBufferConfig(
|
|
32
|
+
maxSize = bufferObj.optInt("maxSize", 1000)
|
|
33
|
+
)
|
|
34
|
+
} else {
|
|
35
|
+
null
|
|
36
|
+
}
|
|
37
|
+
return BGLHttpConfig(
|
|
38
|
+
url = httpObj.getString("url") ?: "",
|
|
39
|
+
headers = headers,
|
|
40
|
+
buffer = bufferConfig
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse notification config from the "notification" object.
|
|
46
|
+
* Returns defaults if not provided.
|
|
47
|
+
*/
|
|
48
|
+
fun parseNotificationConfig(options: JSONObject): BGLNotificationConfig {
|
|
49
|
+
val notifObj = options.optJSONObject("notification") ?: return BGLNotificationConfig()
|
|
50
|
+
return BGLNotificationConfig(
|
|
51
|
+
title = notifObj.optString("title", "Background Location"),
|
|
52
|
+
text = notifObj.optString("text", "Tracking your location")
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse distance filter mode and value.
|
|
58
|
+
* Returns a pair of (distanceFilterMode, distanceFilter).
|
|
59
|
+
*/
|
|
60
|
+
fun parseDistanceFilter(options: JSONObject): Pair<String, Float> {
|
|
61
|
+
val distanceFilterRaw = options.optString("distanceFilter", null)
|
|
62
|
+
val isAutoMode = distanceFilterRaw == "auto"
|
|
63
|
+
val mode = if (isAutoMode) "auto" else "fixed"
|
|
64
|
+
val value = if (isAutoMode) {
|
|
65
|
+
BGLAutoDistanceFilterConfig.DEFAULT_MIN_DISTANCE
|
|
66
|
+
} else {
|
|
67
|
+
options.optDouble("distanceFilter", 15.0).toFloat()
|
|
68
|
+
}
|
|
69
|
+
return Pair(mode, value)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse auto distance filter config.
|
|
74
|
+
* Returns null if not in auto mode.
|
|
75
|
+
*/
|
|
76
|
+
fun parseAutoDistanceFilterConfig(options: JSONObject, isAutoMode: Boolean): BGLAutoDistanceFilterConfig? {
|
|
77
|
+
if (!isAutoMode) return null
|
|
78
|
+
val autoObj = options.optJSONObject("autoDistanceFilter")
|
|
79
|
+
return if (autoObj != null) {
|
|
80
|
+
BGLAutoDistanceFilterConfig(
|
|
81
|
+
targetInterval = autoObj.optDouble("targetInterval", BGLAutoDistanceFilterConfig.DEFAULT_TARGET_INTERVAL.toDouble()),
|
|
82
|
+
minDistance = autoObj.optDouble("minDistance", BGLAutoDistanceFilterConfig.DEFAULT_MIN_DISTANCE.toDouble()).toFloat(),
|
|
83
|
+
maxDistance = autoObj.optDouble("maxDistance", BGLAutoDistanceFilterConfig.DEFAULT_MAX_DISTANCE.toDouble()).toFloat()
|
|
84
|
+
)
|
|
85
|
+
} else {
|
|
86
|
+
BGLAutoDistanceFilterConfig()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build the full BGLLocationConfig from a JSONObject options dictionary.
|
|
92
|
+
*/
|
|
93
|
+
fun parseLocationConfig(options: JSONObject): BGLLocationConfig {
|
|
94
|
+
val httpConfig = parseHttpConfig(options)
|
|
95
|
+
val notificationConfig = parseNotificationConfig(options)
|
|
96
|
+
val (distanceFilterMode, distanceFilter) = parseDistanceFilter(options)
|
|
97
|
+
val isAutoMode = distanceFilterMode == "auto"
|
|
98
|
+
val autoDistanceFilterConfig = parseAutoDistanceFilterConfig(options, isAutoMode)
|
|
99
|
+
|
|
100
|
+
return BGLLocationConfig(
|
|
101
|
+
distanceFilter = distanceFilter,
|
|
102
|
+
distanceFilterMode = distanceFilterMode,
|
|
103
|
+
autoDistanceFilter = autoDistanceFilterConfig,
|
|
104
|
+
desiredAccuracy = options.optString("desiredAccuracy", "high"),
|
|
105
|
+
locationUpdateInterval = options.optLong("locationUpdateInterval", 5000L),
|
|
106
|
+
fastestLocationUpdateInterval = options.optLong("fastestLocationUpdateInterval", 2000L),
|
|
107
|
+
heartbeatInterval = options.optInt("heartbeatInterval", 15),
|
|
108
|
+
http = httpConfig,
|
|
109
|
+
notification = notificationConfig,
|
|
110
|
+
debug = options.optBoolean("debug", false),
|
|
111
|
+
debugSounds = options.optBoolean("debugSounds", false)
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
package dev.bglocation.core.debug
|
|
2
|
+
|
|
3
|
+
import android.media.ToneGenerator
|
|
4
|
+
import android.media.AudioManager
|
|
5
|
+
import android.util.Log
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Debug logger for the background location plugin.
|
|
9
|
+
*
|
|
10
|
+
* When [enabled], emits detailed logs via [Log.d] and fires [onDebug] callbacks
|
|
11
|
+
* with human-readable messages. Optionally plays short system sounds when
|
|
12
|
+
* [soundsEnabled] is true — useful for confirming background location events
|
|
13
|
+
* without looking at the screen.
|
|
14
|
+
*/
|
|
15
|
+
class BGLDebugLogger {
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
private const val TAG = "BGL_DEBUG"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var enabled = false
|
|
22
|
+
private set
|
|
23
|
+
var soundsEnabled = false
|
|
24
|
+
private set
|
|
25
|
+
|
|
26
|
+
var onDebug: ((String) -> Unit)? = null
|
|
27
|
+
|
|
28
|
+
var locationCount = 0
|
|
29
|
+
private set
|
|
30
|
+
var heartbeatCount = 0
|
|
31
|
+
private set
|
|
32
|
+
var httpSuccessCount = 0
|
|
33
|
+
private set
|
|
34
|
+
var httpErrorCount = 0
|
|
35
|
+
private set
|
|
36
|
+
|
|
37
|
+
private var toneGenerator: ToneGenerator? = null
|
|
38
|
+
|
|
39
|
+
fun configure(debug: Boolean, debugSounds: Boolean) {
|
|
40
|
+
enabled = debug
|
|
41
|
+
soundsEnabled = debugSounds
|
|
42
|
+
if (debug && debugSounds) {
|
|
43
|
+
toneGenerator = try {
|
|
44
|
+
ToneGenerator(AudioManager.STREAM_MUSIC, 100)
|
|
45
|
+
} catch (_: Exception) {
|
|
46
|
+
null
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
toneGenerator?.release()
|
|
50
|
+
toneGenerator = null
|
|
51
|
+
}
|
|
52
|
+
reset()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fun reset() {
|
|
56
|
+
locationCount = 0
|
|
57
|
+
heartbeatCount = 0
|
|
58
|
+
httpSuccessCount = 0
|
|
59
|
+
httpErrorCount = 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fun logConfigure(distanceFilter: Float, heartbeat: Int, httpUrl: String?) {
|
|
63
|
+
if (!enabled) return
|
|
64
|
+
val httpInfo = httpUrl ?: "disabled"
|
|
65
|
+
emit("CONFIGURE distance=${distanceFilter}m heartbeat=${heartbeat}s http=$httpInfo")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fun logStart() {
|
|
69
|
+
if (!enabled) return
|
|
70
|
+
reset()
|
|
71
|
+
emit("START tracking")
|
|
72
|
+
playTone(ToneGenerator.TONE_PROP_BEEP)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fun logStop() {
|
|
76
|
+
if (!enabled) return
|
|
77
|
+
emit("STOP tracking — locations=$locationCount heartbeats=$heartbeatCount http_ok=$httpSuccessCount http_err=$httpErrorCount")
|
|
78
|
+
playTone(ToneGenerator.TONE_PROP_BEEP2)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun logLocation(lat: Double, lng: Double, accuracy: Float, speed: Float, isMoving: Boolean) {
|
|
82
|
+
if (!enabled) return
|
|
83
|
+
locationCount++
|
|
84
|
+
val moving = if (isMoving) "moving" else "stationary"
|
|
85
|
+
emit("LOCATION #$locationCount (${"%.5f".format(lat)}, ${"%.5f".format(lng)}) acc=${"%.1f".format(accuracy)}m spd=${"%.1f".format(speed)}m/s $moving")
|
|
86
|
+
playTone(ToneGenerator.TONE_PROP_ACK)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fun logHeartbeat(hasLocation: Boolean) {
|
|
90
|
+
if (!enabled) return
|
|
91
|
+
heartbeatCount++
|
|
92
|
+
val status = if (hasLocation) "with location" else "no location"
|
|
93
|
+
emit("HEARTBEAT #$heartbeatCount $status")
|
|
94
|
+
playTone(ToneGenerator.TONE_SUP_CONFIRM)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fun logHttp(statusCode: Int, success: Boolean, error: String?) {
|
|
98
|
+
if (!enabled) return
|
|
99
|
+
if (success) {
|
|
100
|
+
httpSuccessCount++
|
|
101
|
+
emit("HTTP OK #$httpSuccessCount status=$statusCode")
|
|
102
|
+
} else {
|
|
103
|
+
httpErrorCount++
|
|
104
|
+
val errMsg = error ?: "unknown"
|
|
105
|
+
emit("HTTP ERROR #$httpErrorCount status=$statusCode error=$errMsg")
|
|
106
|
+
playTone(ToneGenerator.TONE_SUP_ERROR)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fun logMessage(message: String) {
|
|
111
|
+
if (!enabled) return
|
|
112
|
+
emit(message)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// MARK: - Geofence Logging
|
|
116
|
+
|
|
117
|
+
fun logGeofenceAdd(identifier: String) {
|
|
118
|
+
if (!enabled) return
|
|
119
|
+
emit("GEOFENCE ADD $identifier")
|
|
120
|
+
playTone(ToneGenerator.TONE_PROP_ACK)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fun logGeofenceAddBatch(count: Int) {
|
|
124
|
+
if (!enabled) return
|
|
125
|
+
emit("GEOFENCE ADD_BATCH count=$count")
|
|
126
|
+
playTone(ToneGenerator.TONE_PROP_ACK)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fun logGeofenceRemove(identifier: String) {
|
|
130
|
+
if (!enabled) return
|
|
131
|
+
emit("GEOFENCE REMOVE $identifier")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fun logGeofenceRemoveAll(count: Int) {
|
|
135
|
+
if (!enabled) return
|
|
136
|
+
emit("GEOFENCE REMOVE_ALL count=$count")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fun logGeofenceEvent(identifier: String, action: String) {
|
|
140
|
+
if (!enabled) return
|
|
141
|
+
emit("GEOFENCE ${action.uppercase()} $identifier")
|
|
142
|
+
playTone(ToneGenerator.TONE_SUP_CONFIRM)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Build a summary line for the dynamic notification. */
|
|
146
|
+
fun notificationText(lat: Double?, lng: Double?, effectiveDistanceFilter: Float? = null): String {
|
|
147
|
+
val coordsPart = if (lat != null && lng != null) {
|
|
148
|
+
"${"%.4f".format(lat)}, ${"%.4f".format(lng)}"
|
|
149
|
+
} else {
|
|
150
|
+
"waiting for GPS"
|
|
151
|
+
}
|
|
152
|
+
val distPart = if (effectiveDistanceFilter != null) " df:${effectiveDistanceFilter.toInt()}m" else ""
|
|
153
|
+
return "$coordsPart | L:$locationCount H:$heartbeatCount OK:$httpSuccessCount ERR:$httpErrorCount$distPart"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private fun emit(message: String) {
|
|
157
|
+
Log.d(TAG, message)
|
|
158
|
+
onDebug?.invoke(message)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private fun playTone(tone: Int) {
|
|
162
|
+
if (!soundsEnabled) return
|
|
163
|
+
try {
|
|
164
|
+
toneGenerator?.startTone(tone, 150)
|
|
165
|
+
} catch (_: Exception) {
|
|
166
|
+
// Ignore — sound is best-effort
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fun release() {
|
|
171
|
+
toneGenerator?.release()
|
|
172
|
+
toneGenerator = null
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
package dev.bglocation.core.geofence
|
|
2
|
+
|
|
3
|
+
import dev.bglocation.core.debug.BGLDebugLogger
|
|
4
|
+
import dev.bglocation.core.location.BGLLocationHelpers
|
|
5
|
+
import android.content.BroadcastReceiver
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.content.Intent
|
|
8
|
+
import android.util.Log
|
|
9
|
+
import com.google.android.gms.location.Geofence
|
|
10
|
+
import com.google.android.gms.location.GeofencingEvent
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* BroadcastReceiver for geofence transition events from [GeofencingClient].
|
|
14
|
+
*
|
|
15
|
+
* Registered in AndroidManifest.xml as a static receiver so it fires even when the app
|
|
16
|
+
* is not in the foreground. The receiver resolves the [BGLGeofenceManager] singleton
|
|
17
|
+
* from the companion object (set by [BackgroundLocationPlugin] on configure).
|
|
18
|
+
*/
|
|
19
|
+
class BGLGeofenceBroadcastReceiver : BroadcastReceiver() {
|
|
20
|
+
|
|
21
|
+
companion object {
|
|
22
|
+
private const val TAG = "BGLocation"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Singleton reference set by [BackgroundLocationPlugin.configure].
|
|
26
|
+
* The receiver uses this to look up extras and emit events.
|
|
27
|
+
*/
|
|
28
|
+
@Volatile
|
|
29
|
+
var geofenceManager: BGLGeofenceManager? = null
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Debug logger reference set by [BackgroundLocationPlugin.configure].
|
|
33
|
+
* Used to log geofence transition events with sounds.
|
|
34
|
+
*/
|
|
35
|
+
@Volatile
|
|
36
|
+
var debugLogger: BGLDebugLogger? = null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
40
|
+
if (intent.action != BGLGeofenceManager.ACTION_GEOFENCE_EVENT) return
|
|
41
|
+
|
|
42
|
+
val event = GeofencingEvent.fromIntent(intent)
|
|
43
|
+
if (event == null) {
|
|
44
|
+
Log.w(TAG, "BGLGeofenceBroadcastReceiver: null GeofencingEvent")
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
if (event.hasError()) {
|
|
48
|
+
Log.e(TAG, "BGLGeofenceBroadcastReceiver error: ${event.errorCode}")
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
val triggeringGeofences = event.triggeringGeofences ?: return
|
|
53
|
+
val transitionType = event.geofenceTransition
|
|
54
|
+
|
|
55
|
+
val action = when (transitionType) {
|
|
56
|
+
Geofence.GEOFENCE_TRANSITION_ENTER -> "enter"
|
|
57
|
+
Geofence.GEOFENCE_TRANSITION_EXIT -> "exit"
|
|
58
|
+
Geofence.GEOFENCE_TRANSITION_DWELL -> "dwell"
|
|
59
|
+
else -> {
|
|
60
|
+
Log.w(TAG, "BGLGeofenceBroadcastReceiver: unknown transition type $transitionType")
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
val triggeringLocation = event.triggeringLocation
|
|
66
|
+
val locationData = if (triggeringLocation != null) {
|
|
67
|
+
BGLLocationHelpers.mapLocation(triggeringLocation)
|
|
68
|
+
} else {
|
|
69
|
+
null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
val manager = geofenceManager
|
|
73
|
+
if (manager == null) {
|
|
74
|
+
Log.w(TAG, "BGLGeofenceBroadcastReceiver: geofenceManager is null — plugin not configured. Dropping ${triggeringGeofences.size} event(s).")
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
val now = System.currentTimeMillis()
|
|
78
|
+
|
|
79
|
+
for (geofence in triggeringGeofences) {
|
|
80
|
+
val config = manager.findGeofence(geofence.requestId)
|
|
81
|
+
val eventData = BGLGeofenceEventData(
|
|
82
|
+
identifier = geofence.requestId,
|
|
83
|
+
action = action,
|
|
84
|
+
location = locationData,
|
|
85
|
+
extras = config?.extras,
|
|
86
|
+
timestamp = now
|
|
87
|
+
)
|
|
88
|
+
Log.d(TAG, "Geofence $action: ${geofence.requestId}")
|
|
89
|
+
debugLogger?.logGeofenceEvent(geofence.requestId, action)
|
|
90
|
+
manager.onGeofenceEvent?.invoke(eventData)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|