@bricks-soft/capacitor-location-tracking 0.0.0-stage → 8.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.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2759 -2
  3. package/android/build.gradle +106 -0
  4. package/android/consumer-rules.pro +42 -0
  5. package/android/src/main/AndroidManifest.xml +52 -0
  6. package/android/src/main/java/com/brickssoft/locationtracking/LocationTrackingPlugin.kt +431 -0
  7. package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingListener.kt +52 -0
  8. package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingNative.kt +267 -0
  9. package/android/src/main/java/com/brickssoft/locationtracking/api/NativeListeners.kt +270 -0
  10. package/android/src/main/java/com/brickssoft/locationtracking/api/NativeThread.kt +57 -0
  11. package/android/src/main/java/com/brickssoft/locationtracking/bridge/BridgeServices.kt +64 -0
  12. package/android/src/main/java/com/brickssoft/locationtracking/bridge/CallResponder.kt +41 -0
  13. package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +281 -0
  14. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +541 -0
  15. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginPermissionHost.kt +76 -0
  16. package/android/src/main/java/com/brickssoft/locationtracking/bridge/WatchRegistry.kt +84 -0
  17. package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +231 -0
  18. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +574 -0
  19. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +31 -0
  20. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +170 -0
  21. package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +73 -0
  22. package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +135 -0
  23. package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +45 -0
  24. package/android/src/main/java/com/brickssoft/locationtracking/core/AppDispatchers.kt +21 -0
  25. package/android/src/main/java/com/brickssoft/locationtracking/core/Clock.kt +41 -0
  26. package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +217 -0
  27. package/android/src/main/java/com/brickssoft/locationtracking/core/Constants.kt +78 -0
  28. package/android/src/main/java/com/brickssoft/locationtracking/core/Errors.kt +27 -0
  29. package/android/src/main/java/com/brickssoft/locationtracking/core/EventBus.kt +39 -0
  30. package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +63 -0
  31. package/android/src/main/java/com/brickssoft/locationtracking/core/Iso8601.kt +63 -0
  32. package/android/src/main/java/com/brickssoft/locationtracking/core/Logger.kt +68 -0
  33. package/android/src/main/java/com/brickssoft/locationtracking/core/RecordHooks.kt +47 -0
  34. package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +64 -0
  35. package/android/src/main/java/com/brickssoft/locationtracking/data/SqlSupport.kt +79 -0
  36. package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteGeofenceStore.kt +144 -0
  37. package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteLocationStore.kt +215 -0
  38. package/android/src/main/java/com/brickssoft/locationtracking/data/Stores.kt +45 -0
  39. package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +111 -0
  40. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +104 -0
  41. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceMonitor.kt +335 -0
  42. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceMonitor.kt +36 -0
  43. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceStateMapping.kt +69 -0
  44. package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +1281 -0
  45. package/android/src/main/java/com/brickssoft/locationtracking/engine/ElasticDistance.kt +68 -0
  46. package/android/src/main/java/com/brickssoft/locationtracking/engine/LocationRequests.kt +74 -0
  47. package/android/src/main/java/com/brickssoft/locationtracking/engine/MotionStateMachine.kt +291 -0
  48. package/android/src/main/java/com/brickssoft/locationtracking/engine/StationaryRegion.kt +41 -0
  49. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +60 -0
  50. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +90 -0
  51. package/android/src/main/java/com/brickssoft/locationtracking/geofence/DefaultGeofenceManager.kt +899 -0
  52. package/android/src/main/java/com/brickssoft/locationtracking/geofence/DwellTracker.kt +80 -0
  53. package/android/src/main/java/com/brickssoft/locationtracking/geofence/GeofenceManager.kt +33 -0
  54. package/android/src/main/java/com/brickssoft/locationtracking/geofence/PolygonMath.kt +194 -0
  55. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +713 -0
  56. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +69 -0
  57. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarms.kt +100 -0
  58. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatScheduler.kt +25 -0
  59. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatWindow.kt +179 -0
  60. package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +213 -0
  61. package/android/src/main/java/com/brickssoft/locationtracking/http/BodyBuilder.kt +73 -0
  62. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +118 -0
  63. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +18 -0
  64. package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +551 -0
  65. package/android/src/main/java/com/brickssoft/locationtracking/http/StrictJson.kt +135 -0
  66. package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +109 -0
  67. package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +143 -0
  68. package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +461 -0
  69. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFileProvider.kt +9 -0
  70. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFormatter.kt +148 -0
  71. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogStore.kt +30 -0
  72. package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +176 -0
  73. package/android/src/main/java/com/brickssoft/locationtracking/model/GeofenceSpec.kt +25 -0
  74. package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +550 -0
  75. package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +106 -0
  76. package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +53 -0
  77. package/android/src/main/java/com/brickssoft/locationtracking/permission/BackgroundRationaleDialog.kt +85 -0
  78. package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt +72 -0
  79. package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt +333 -0
  80. package/android/src/main/java/com/brickssoft/locationtracking/permission/PermissionManager.kt +54 -0
  81. package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +277 -0
  82. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionService.kt +39 -0
  83. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +115 -0
  84. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt +93 -0
  85. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultOdometer.kt +99 -0
  86. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +166 -0
  87. package/android/src/main/java/com/brickssoft/locationtracking/processing/Geo.kt +56 -0
  88. package/android/src/main/java/com/brickssoft/locationtracking/processing/KalmanFilter.kt +72 -0
  89. package/android/src/main/java/com/brickssoft/locationtracking/processing/Processing.kt +48 -0
  90. package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +187 -0
  91. package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +169 -0
  92. package/android/src/main/java/com/brickssoft/locationtracking/provider/ProviderPackaging.kt +135 -0
  93. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidActivityBackend.kt +25 -0
  94. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceBackend.kt +181 -0
  95. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceReceiver.kt +115 -0
  96. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +265 -0
  97. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidProviderBundle.kt +31 -0
  98. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +98 -0
  99. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt +80 -0
  100. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt +62 -0
  101. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsErrors.kt +72 -0
  102. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt +152 -0
  103. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt +78 -0
  104. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +176 -0
  105. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +62 -0
  106. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt +88 -0
  107. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsTasks.kt +55 -0
  108. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt +71 -0
  109. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt +57 -0
  110. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsErrors.kt +76 -0
  111. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt +116 -0
  112. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt +67 -0
  113. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsIntents.kt +83 -0
  114. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +197 -0
  115. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +55 -0
  116. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsTasks.kt +44 -0
  117. package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +95 -0
  118. package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +44 -0
  119. package/android/src/main/java/com/brickssoft/locationtracking/service/BootReceiver.kt +175 -0
  120. package/android/src/main/java/com/brickssoft/locationtracking/service/DefaultServiceController.kt +196 -0
  121. package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt +319 -0
  122. package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt +42 -0
  123. package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationFactory.kt +338 -0
  124. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceCommands.kt +204 -0
  125. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceController.kt +22 -0
  126. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceDeps.kt +61 -0
  127. package/android/src/main/java/com/brickssoft/locationtracking/service/WhileInUseStartCheck.kt +115 -0
  128. package/android/src/main/java/com/brickssoft/locationtracking/settings/DefaultDeviceSettings.kt +79 -0
  129. package/android/src/main/java/com/brickssoft/locationtracking/settings/DeviceSettings.kt +21 -0
  130. package/android/src/main/java/com/brickssoft/locationtracking/settings/OemPowerManagers.kt +198 -0
  131. package/android/src/main/res/drawable/lt_ic_notification.xml +11 -0
  132. package/android/src/main/res/values/lt_strings.xml +11 -0
  133. package/android/src/main/res/xml/lt_file_paths.xml +5 -0
  134. package/dist/esm/definitions.d.ts +719 -0
  135. package/dist/esm/definitions.js +2 -0
  136. package/dist/esm/definitions.js.map +1 -0
  137. package/dist/esm/events.d.ts +50 -0
  138. package/dist/esm/events.js +52 -0
  139. package/dist/esm/events.js.map +1 -0
  140. package/dist/esm/index.d.ts +3 -0
  141. package/dist/esm/index.js +5 -0
  142. package/dist/esm/index.js.map +1 -0
  143. package/dist/esm/plugin.d.ts +3 -0
  144. package/dist/esm/plugin.js +10 -0
  145. package/dist/esm/plugin.js.map +1 -0
  146. package/dist/esm/web/config.d.ts +19 -0
  147. package/dist/esm/web/config.js +243 -0
  148. package/dist/esm/web/config.js.map +1 -0
  149. package/dist/esm/web/device.d.ts +9 -0
  150. package/dist/esm/web/device.js +90 -0
  151. package/dist/esm/web/device.js.map +1 -0
  152. package/dist/esm/web/errors.d.ts +14 -0
  153. package/dist/esm/web/errors.js +19 -0
  154. package/dist/esm/web/errors.js.map +1 -0
  155. package/dist/esm/web/geolocation.d.ts +26 -0
  156. package/dist/esm/web/geolocation.js +117 -0
  157. package/dist/esm/web/geolocation.js.map +1 -0
  158. package/dist/esm/web/location.d.ts +7 -0
  159. package/dist/esm/web/location.js +70 -0
  160. package/dist/esm/web/location.js.map +1 -0
  161. package/dist/esm/web.d.ts +164 -0
  162. package/dist/esm/web.js +328 -0
  163. package/dist/esm/web.js.map +1 -0
  164. package/dist/plugin.cjs.js +945 -0
  165. package/dist/plugin.cjs.js.map +1 -0
  166. package/dist/plugin.js +948 -0
  167. package/dist/plugin.js.map +1 -0
  168. package/package.json +59 -4
@@ -0,0 +1,53 @@
1
+ package com.brickssoft.locationtracking.model
2
+
3
+ import android.os.Build
4
+
5
+ /**
6
+ * A location fix, independent of the backend that produced it.
7
+ * Optional fields are null when the fix does not carry them.
8
+ *
9
+ * @property time fix time, epoch ms.
10
+ * @property elapsedRealtimeNanos fix time on the elapsed-realtime clock (0 if unknown). Not part of the wire format.
11
+ * @property provider OS provider name (e.g. "gps", "fused"). Not part of the wire format.
12
+ */
13
+ data class TrackedLocation(
14
+ val latitude: Double,
15
+ val longitude: Double,
16
+ val accuracy: Float,
17
+ val altitude: Double? = null,
18
+ val altitudeAccuracy: Float? = null,
19
+ val speed: Float? = null,
20
+ val speedAccuracy: Float? = null,
21
+ val heading: Float? = null,
22
+ val headingAccuracy: Float? = null,
23
+ val time: Long,
24
+ val elapsedRealtimeNanos: Long = 0,
25
+ val provider: String? = null,
26
+ val isMock: Boolean = false,
27
+ ) {
28
+ companion object {
29
+ /** Converts an Android [android.location.Location], keeping only the fields it actually has. */
30
+ fun from(l: android.location.Location): TrackedLocation {
31
+ val api26 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
32
+ return TrackedLocation(
33
+ latitude = l.latitude,
34
+ longitude = l.longitude,
35
+ accuracy = l.accuracy,
36
+ altitude = if (l.hasAltitude()) l.altitude else null,
37
+ altitudeAccuracy = if (api26 && l.hasVerticalAccuracy()) l.verticalAccuracyMeters else null,
38
+ speed = if (l.hasSpeed()) l.speed else null,
39
+ speedAccuracy = if (api26 && l.hasSpeedAccuracy()) l.speedAccuracyMetersPerSecond else null,
40
+ heading = if (l.hasBearing()) l.bearing else null,
41
+ headingAccuracy = if (api26 && l.hasBearingAccuracy()) l.bearingAccuracyDegrees else null,
42
+ time = l.time,
43
+ elapsedRealtimeNanos = l.elapsedRealtimeNanos,
44
+ provider = l.provider,
45
+ isMock = isMock(l),
46
+ )
47
+ }
48
+
49
+ @Suppress("DEPRECATION")
50
+ private fun isMock(l: android.location.Location): Boolean =
51
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) l.isMock else l.isFromMockProvider
52
+ }
53
+ }
@@ -0,0 +1,85 @@
1
+ package com.brickssoft.locationtracking.permission
2
+
3
+ import android.app.Activity
4
+ import android.app.AlertDialog
5
+ import android.app.Application
6
+ import android.content.Context
7
+ import com.brickssoft.locationtracking.R
8
+ import com.brickssoft.locationtracking.config.BackgroundPermissionRationale
9
+ import com.brickssoft.locationtracking.core.Logger
10
+
11
+ /**
12
+ * The rationale AlertDialog shown on API 30+ before background location is requested (which makes
13
+ * Android open the app's location-permission settings page, where the user picks "Allow all the time").
14
+ * Texts come from [BackgroundPermissionRationale]; blank or missing values fall back to the
15
+ * `lt_bg_rationale_*` string resources.
16
+ */
17
+ internal object BackgroundRationaleDialog {
18
+ private const val TAG = "LT.Permissions"
19
+
20
+ /** Resolved dialog texts. */
21
+ data class Texts(val title: String, val message: String, val positive: String, val negative: String)
22
+
23
+ fun texts(context: Context, rationale: BackgroundPermissionRationale): Texts = Texts(
24
+ title = rationale.title.orResource(context, R.string.lt_bg_rationale_title),
25
+ message = rationale.message.orResource(context, R.string.lt_bg_rationale_message),
26
+ positive = rationale.positiveAction.orResource(context, R.string.lt_bg_rationale_positive),
27
+ negative = rationale.negativeAction.orResource(context, R.string.lt_bg_rationale_negative),
28
+ )
29
+
30
+ /**
31
+ * Shows the dialog on [activity] (main thread). [onResult] runs exactly once on the main thread: true for
32
+ * the positive button; false for the negative button, back/outside cancel, any other dismissal, or the
33
+ * Activity being destroyed while the dialog is up. Returns false (and never calls [onResult]) if the
34
+ * dialog could not be shown.
35
+ */
36
+ fun show(activity: Activity, rationale: BackgroundPermissionRationale, onResult: (Boolean) -> Unit): Boolean {
37
+ val application: Application? = activity.application
38
+ var watcher: Application.ActivityLifecycleCallbacks? = null
39
+ var resolved = false
40
+ val resolve = { accepted: Boolean ->
41
+ if (!resolved) {
42
+ resolved = true
43
+ watcher?.let { application?.unregisterActivityLifecycleCallbacks(it) }
44
+ onResult(accepted)
45
+ }
46
+ }
47
+ return try {
48
+ val texts = texts(activity, rationale)
49
+ val dialog = AlertDialog.Builder(activity)
50
+ .setTitle(texts.title)
51
+ .setMessage(texts.message)
52
+ .setPositiveButton(texts.positive) { _, _ -> resolve(true) }
53
+ .setNegativeButton(texts.negative) { _, _ -> resolve(false) }
54
+ .setOnCancelListener { resolve(false) }
55
+ .setOnDismissListener { resolve(false) }
56
+ .create()
57
+ // A destroyed Activity drops its windows without dismissing the dialog; never leave the caller hanging.
58
+ val owner = activity
59
+ val destroyWatcher = object : ActivityLifecycleAdapter() {
60
+ override fun onActivityDestroyed(activity: Activity) {
61
+ if (activity !== owner) return
62
+ resolve(false)
63
+ try {
64
+ dialog.dismiss()
65
+ } catch (e: RuntimeException) {
66
+ Logger.d(TAG, "rationale dialog already gone", e)
67
+ }
68
+ }
69
+ }
70
+ watcher = destroyWatcher
71
+ application?.registerActivityLifecycleCallbacks(destroyWatcher)
72
+ dialog.show()
73
+ true
74
+ } catch (e: RuntimeException) {
75
+ // e.g. WindowManager.BadTokenException when the activity window is gone
76
+ resolved = true
77
+ watcher?.let { application?.unregisterActivityLifecycleCallbacks(it) }
78
+ Logger.w(TAG, "cannot show the background-location rationale dialog", e)
79
+ false
80
+ }
81
+ }
82
+
83
+ private fun String?.orResource(context: Context, resId: Int): String =
84
+ this?.takeIf { it.isNotBlank() } ?: context.getString(resId)
85
+ }
@@ -0,0 +1,72 @@
1
+ package com.brickssoft.locationtracking.permission
2
+
3
+ import android.app.Activity
4
+ import android.app.Application
5
+ import android.content.Context
6
+ import android.os.Bundle
7
+ import java.lang.ref.WeakReference
8
+
9
+ /** [Application.ActivityLifecycleCallbacks] with no-op defaults. */
10
+ internal abstract class ActivityLifecycleAdapter : Application.ActivityLifecycleCallbacks {
11
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
12
+
13
+ override fun onActivityStarted(activity: Activity) = Unit
14
+
15
+ override fun onActivityResumed(activity: Activity) = Unit
16
+
17
+ override fun onActivityPaused(activity: Activity) = Unit
18
+
19
+ override fun onActivityStopped(activity: Activity) = Unit
20
+
21
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
22
+
23
+ override fun onActivityDestroyed(activity: Activity) = Unit
24
+ }
25
+
26
+ /**
27
+ * Weakly remembers the most recently created, started or resumed Activity of the app, and the last
28
+ * [PermissionHost] Activity, so `shouldShowRequestPermissionRationale` can be asked outside a request.
29
+ * One tracker per Application ([attach]).
30
+ */
31
+ internal class CurrentActivityTracker : ActivityLifecycleAdapter() {
32
+ @Volatile
33
+ private var ref: WeakReference<Activity>? = null
34
+
35
+ /** The remembered Activity, or null if there is none or it is finishing or destroyed. */
36
+ fun current(): Activity? = ref?.get()?.takeIf { it.isUsable() }
37
+
38
+ fun remember(activity: Activity) {
39
+ if (ref?.get() !== activity) ref = WeakReference(activity)
40
+ }
41
+
42
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = remember(activity)
43
+
44
+ override fun onActivityStarted(activity: Activity) = remember(activity)
45
+
46
+ override fun onActivityResumed(activity: Activity) = remember(activity)
47
+
48
+ override fun onActivityDestroyed(activity: Activity) {
49
+ if (ref?.get() === activity) ref = null
50
+ }
51
+
52
+ companion object {
53
+ private var sharedApp: Application? = null
54
+ private var shared: CurrentActivityTracker? = null
55
+
56
+ /** The tracker registered on [context]'s Application (created and registered on first use). */
57
+ @Synchronized
58
+ fun attach(context: Context): CurrentActivityTracker {
59
+ val app = context.applicationContext as? Application ?: context as? Application
60
+ ?: return CurrentActivityTracker()
61
+ shared?.let { if (sharedApp === app) return it }
62
+ val tracker = CurrentActivityTracker()
63
+ app.registerActivityLifecycleCallbacks(tracker)
64
+ sharedApp = app
65
+ shared = tracker
66
+ return tracker
67
+ }
68
+ }
69
+ }
70
+
71
+ /** True if [this] can still host a dialog or answer permission-rationale queries. */
72
+ internal fun Activity.isUsable(): Boolean = !isFinishing && !isDestroyed
@@ -0,0 +1,333 @@
1
+ package com.brickssoft.locationtracking.permission
2
+
3
+ import android.Manifest
4
+ import android.app.Activity
5
+ import android.content.Context
6
+ import android.content.SharedPreferences
7
+ import android.content.pm.PackageManager
8
+ import android.os.Build
9
+ import android.os.Handler
10
+ import android.os.Looper
11
+ import androidx.annotation.VisibleForTesting
12
+ import androidx.core.app.ActivityCompat
13
+ import androidx.core.content.ContextCompat
14
+ import com.brickssoft.locationtracking.config.BackgroundPermissionRationale
15
+ import com.brickssoft.locationtracking.core.Constants
16
+ import com.brickssoft.locationtracking.core.Logger
17
+
18
+ /**
19
+ * API-level-aware [PermissionManager].
20
+ *
21
+ * Status: location is granted if fine or coarse is granted. Permissions that do not exist on the device's
22
+ * API level are mapped: background location below API 29 equals location; activity recognition below 29
23
+ * and notifications below 33 are granted. A permission that is not granted is `PROMPT_WITH_RATIONALE` if
24
+ * Android says a rationale should be shown, `PROMPT` if the plugin never asked for it, otherwise `DENIED`.
25
+ * "Asked" flags (scoped to the current install, cleared once granted) and the last rationale answer (used
26
+ * when no Activity is at hand) are kept in the plugin prefs under `perm_` keys.
27
+ *
28
+ * Request order: location, notifications, activity recognition, background location — one alias group at
29
+ * a time, skipping granted and not-applicable types. Background location is requested only once foreground
30
+ * location is granted: directly on API 29, after [BackgroundRationaleDialog] on API 30+. Overlapping
31
+ * `request` calls run one after another.
32
+ *
33
+ * @param activityProvider optional source of the Activity used for `shouldShowRequestPermissionRationale`.
34
+ * When it returns null, the app's last started/resumed Activity or the last [PermissionHost] Activity is used.
35
+ */
36
+ class DefaultPermissionManager @VisibleForTesting internal constructor(
37
+ context: Context,
38
+ private val activityProvider: () -> Activity?,
39
+ private val sdkInt: Int,
40
+ ) : PermissionManager {
41
+ constructor(context: Context, activityProvider: () -> Activity? = { null }) :
42
+ this(context, activityProvider, Build.VERSION.SDK_INT)
43
+
44
+ private val appContext: Context = context.applicationContext ?: context
45
+ private val prefs: SharedPreferences by lazy {
46
+ appContext.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
47
+ }
48
+
49
+ /** Identifies this install, so "asked" flags restored from a backup onto a new install are ignored. */
50
+ private val installMarker: Long by lazy {
51
+ try {
52
+ @Suppress("DEPRECATION")
53
+ appContext.packageManager.getPackageInfo(appContext.packageName, 0).firstInstallTime
54
+ } catch (e: Exception) {
55
+ Logger.w(TAG, "cannot read the install time", e)
56
+ 0L
57
+ }
58
+ }
59
+ private val activityTracker = CurrentActivityTracker.attach(appContext)
60
+ private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
61
+
62
+ // Request flows; main thread only.
63
+ private var activeFlow: RequestFlow? = null
64
+ private val queuedFlows = ArrayDeque<RequestFlow>()
65
+
66
+ override fun status(): Map<PermissionType, PermissionState> {
67
+ val activity = currentActivity()
68
+ return PermissionType.entries.associateWith { stateOf(it, activity) }
69
+ }
70
+
71
+ override fun hasForegroundLocation(): Boolean =
72
+ isGranted(Manifest.permission.ACCESS_FINE_LOCATION) || isGranted(Manifest.permission.ACCESS_COARSE_LOCATION)
73
+
74
+ override fun hasBackgroundLocation(): Boolean = if (sdkInt < Build.VERSION_CODES.Q) {
75
+ hasForegroundLocation()
76
+ } else {
77
+ hasForegroundLocation() && isGranted(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
78
+ }
79
+
80
+ override fun hasActivityRecognition(): Boolean =
81
+ sdkInt < Build.VERSION_CODES.Q || isGranted(Manifest.permission.ACTIVITY_RECOGNITION)
82
+
83
+ override fun hasNotifications(): Boolean =
84
+ sdkInt < Build.VERSION_CODES.TIRAMISU || isGranted(Manifest.permission.POST_NOTIFICATIONS)
85
+
86
+ override fun request(
87
+ host: PermissionHost,
88
+ types: List<PermissionType>,
89
+ rationale: BackgroundPermissionRationale,
90
+ onDone: (Map<PermissionType, PermissionState>) -> Unit,
91
+ ) {
92
+ if (!isMainThread()) {
93
+ mainHandler.post { request(host, types, rationale, onDone) }
94
+ return
95
+ }
96
+ hostActivity(host)?.let { activityTracker.remember(it) }
97
+ val flow = RequestFlow(host, types.toSet(), rationale, onDone)
98
+ if (activeFlow != null) {
99
+ Logger.d(TAG, "a permission request is in progress; queued ${types.map { it.alias }}")
100
+ queuedFlows.addLast(flow)
101
+ return
102
+ }
103
+ activeFlow = flow
104
+ flow.next()
105
+ }
106
+
107
+ // ---- status
108
+
109
+ private fun stateOf(type: PermissionType, activity: Activity?): PermissionState = when {
110
+ type == PermissionType.BACKGROUND_LOCATION && sdkInt < Build.VERSION_CODES.Q ->
111
+ stateOf(PermissionType.LOCATION, activity)
112
+ isGranted(type) -> PermissionState.GRANTED.also { forget(type) }
113
+ activity != null && shouldShowRationale(type, activity) -> PermissionState.PROMPT_WITH_RATIONALE
114
+ !wasAsked(type) -> PermissionState.PROMPT
115
+ activity == null && lastRationale(type) -> PermissionState.PROMPT_WITH_RATIONALE
116
+ else -> PermissionState.DENIED
117
+ }
118
+
119
+ private fun isGranted(type: PermissionType): Boolean = when (type) {
120
+ PermissionType.LOCATION -> hasForegroundLocation()
121
+ PermissionType.BACKGROUND_LOCATION -> hasBackgroundLocation()
122
+ PermissionType.ACTIVITY_RECOGNITION -> hasActivityRecognition()
123
+ PermissionType.NOTIFICATIONS -> hasNotifications()
124
+ }
125
+
126
+ /** True if [type] is a runtime permission on this API level (otherwise it is implied or not needed). */
127
+ private fun isRuntime(type: PermissionType): Boolean = when (type) {
128
+ PermissionType.LOCATION -> true
129
+ PermissionType.BACKGROUND_LOCATION, PermissionType.ACTIVITY_RECOGNITION -> sdkInt >= Build.VERSION_CODES.Q
130
+ PermissionType.NOTIFICATIONS -> sdkInt >= Build.VERSION_CODES.TIRAMISU
131
+ }
132
+
133
+ private fun isGranted(permission: String): Boolean =
134
+ ContextCompat.checkSelfPermission(appContext, permission) == PackageManager.PERMISSION_GRANTED
135
+
136
+ private fun shouldShowRationale(type: PermissionType, activity: Activity): Boolean =
137
+ isRuntime(type) && androidPermissions(type).any { permission ->
138
+ try {
139
+ ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
140
+ } catch (e: RuntimeException) {
141
+ Logger.w(TAG, "shouldShowRequestPermissionRationale($permission) failed", e)
142
+ false
143
+ }
144
+ }
145
+
146
+ private fun currentActivity(): Activity? =
147
+ activityProvider()?.takeIf { it.isUsable() } ?: activityTracker.current()
148
+
149
+ private fun hostActivity(host: PermissionHost): Activity? =
150
+ try {
151
+ host.activity?.takeIf { it.isUsable() }
152
+ } catch (e: RuntimeException) {
153
+ Logger.w(TAG, "permission host has no activity", e)
154
+ null
155
+ }
156
+
157
+ // ---- persisted flags (plugin prefs, `perm_` keys only)
158
+
159
+ private fun wasAsked(type: PermissionType): Boolean =
160
+ try {
161
+ prefs.getLong(askedKey(type), NEVER) == installMarker
162
+ } catch (e: ClassCastException) {
163
+ false
164
+ }
165
+
166
+ private fun lastRationale(type: PermissionType): Boolean =
167
+ try {
168
+ prefs.getBoolean(rationaleKey(type), false)
169
+ } catch (e: ClassCastException) {
170
+ false
171
+ }
172
+
173
+ private fun markAsked(type: PermissionType) {
174
+ prefs.edit().putLong(askedKey(type), installMarker).apply()
175
+ }
176
+
177
+ /** Drops [type]'s flags: once granted, a later revocation or auto-reset starts from "never asked". */
178
+ private fun forget(type: PermissionType) {
179
+ if (prefs.contains(askedKey(type)) || prefs.contains(rationaleKey(type))) {
180
+ prefs.edit().remove(askedKey(type)).remove(rationaleKey(type)).apply()
181
+ }
182
+ }
183
+
184
+ /** After a request for [type] finished: remember whether Android would show a rationale now. */
185
+ private fun recordResult(type: PermissionType, activity: Activity?) {
186
+ if (activity == null || isGranted(type)) return
187
+ prefs.edit().putBoolean(rationaleKey(type), shouldShowRationale(type, activity)).apply()
188
+ }
189
+
190
+ // ---- request flow
191
+
192
+ private fun onFlowFinished(flow: RequestFlow) {
193
+ if (activeFlow !== flow) return
194
+ val next = queuedFlows.removeFirstOrNull()
195
+ activeFlow = next
196
+ if (next != null) mainHandler.post { next.next() }
197
+ }
198
+
199
+ /** One `request()` call: walks [REQUEST_ORDER] and calls [onDone] exactly once, on the main thread. */
200
+ private inner class RequestFlow(
201
+ private val host: PermissionHost,
202
+ private val types: Set<PermissionType>,
203
+ private val rationale: BackgroundPermissionRationale,
204
+ private val onDone: (Map<PermissionType, PermissionState>) -> Unit,
205
+ ) {
206
+ private val pending = ArrayDeque(REQUEST_ORDER.filter { it in types })
207
+ private var finished = false
208
+
209
+ /** Runs steps until one waits for the user (it calls [next] again later) or none is left. */
210
+ fun next() {
211
+ while (!finished) {
212
+ val type = pending.removeFirstOrNull() ?: return finish()
213
+ val waiting = if (type == PermissionType.BACKGROUND_LOCATION) requestBackground() else requestPlain(type)
214
+ if (waiting) return
215
+ }
216
+ }
217
+
218
+ private fun finish() {
219
+ if (finished) return
220
+ finished = true
221
+ try {
222
+ onDone(status())
223
+ } finally {
224
+ onFlowFinished(this)
225
+ }
226
+ }
227
+
228
+ /** Location, notifications, activity recognition. Returns true if a request is in flight. */
229
+ private fun requestPlain(type: PermissionType): Boolean {
230
+ if (!isRuntime(type)) {
231
+ Logger.d(TAG, "${type.alias}: not a runtime permission on API $sdkInt, skipped")
232
+ return false
233
+ }
234
+ if (isGranted(type)) return false
235
+ return ask(type)
236
+ }
237
+
238
+ private fun requestBackground(): Boolean {
239
+ val type = PermissionType.BACKGROUND_LOCATION
240
+ if (!isRuntime(type)) {
241
+ Logger.d(TAG, "backgroundLocation: implied by location on API $sdkInt, skipped")
242
+ return false
243
+ }
244
+ if (isGranted(type)) return false
245
+ if (!hasForegroundLocation()) {
246
+ Logger.i(TAG, "backgroundLocation: skipped, foreground location is not granted")
247
+ return false
248
+ }
249
+ if (sdkInt < Build.VERSION_CODES.R) return ask(type)
250
+
251
+ val activity = hostActivity(host) ?: currentActivity()
252
+ if (activity == null) {
253
+ Logger.w(TAG, "backgroundLocation: skipped, no activity to show the rationale dialog")
254
+ return false
255
+ }
256
+ return BackgroundRationaleDialog.show(activity, rationale) { accepted ->
257
+ onMain {
258
+ if (accepted) {
259
+ ask(type)
260
+ } else {
261
+ Logger.i(TAG, "backgroundLocation: rationale declined")
262
+ next()
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Requests [type]'s alias through the host; always returns true, the host callback continues the flow.
270
+ * The continuation is posted when the host calls back synchronously or off the main thread, so the rest
271
+ * of the flow never runs inside `requestAliases`. A host that throws leaves [type] "never asked".
272
+ */
273
+ private fun ask(type: PermissionType): Boolean {
274
+ val askedBefore = wasAsked(type)
275
+ markAsked(type)
276
+ var continued = false
277
+ var insideHostCall = true
278
+ val continueFlow = {
279
+ if (!continued) {
280
+ continued = true
281
+ recordResult(type, hostActivity(host) ?: currentActivity())
282
+ next()
283
+ }
284
+ }
285
+ try {
286
+ host.requestAliases(listOf(type.alias)) {
287
+ if (insideHostCall || !isMainThread()) mainHandler.post(continueFlow) else continueFlow()
288
+ }
289
+ } catch (e: RuntimeException) {
290
+ Logger.w(TAG, "requesting ${type.alias} failed", e)
291
+ if (!askedBefore) prefs.edit().remove(askedKey(type)).apply()
292
+ if (!continued) {
293
+ continued = true
294
+ mainHandler.post { next() }
295
+ }
296
+ } finally {
297
+ insideHostCall = false
298
+ }
299
+ return true
300
+ }
301
+ }
302
+
303
+ private fun isMainThread(): Boolean = Looper.myLooper() == Looper.getMainLooper()
304
+
305
+ private fun onMain(block: () -> Unit) {
306
+ if (isMainThread()) block() else mainHandler.post(block)
307
+ }
308
+
309
+ private companion object {
310
+ const val TAG = "LT.Permissions"
311
+ const val KEY_PREFIX = "perm_"
312
+ const val NEVER = -1L
313
+
314
+ val REQUEST_ORDER = listOf(
315
+ PermissionType.LOCATION,
316
+ PermissionType.NOTIFICATIONS,
317
+ PermissionType.ACTIVITY_RECOGNITION,
318
+ PermissionType.BACKGROUND_LOCATION,
319
+ )
320
+
321
+ fun askedKey(type: PermissionType) = "${KEY_PREFIX}asked_${type.alias}"
322
+
323
+ fun rationaleKey(type: PermissionType) = "${KEY_PREFIX}rationale_${type.alias}"
324
+
325
+ fun androidPermissions(type: PermissionType): List<String> = when (type) {
326
+ PermissionType.LOCATION ->
327
+ listOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
328
+ PermissionType.BACKGROUND_LOCATION -> listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
329
+ PermissionType.ACTIVITY_RECOGNITION -> listOf(Manifest.permission.ACTIVITY_RECOGNITION)
330
+ PermissionType.NOTIFICATIONS -> listOf(Manifest.permission.POST_NOTIFICATIONS)
331
+ }
332
+ }
333
+ }
@@ -0,0 +1,54 @@
1
+ package com.brickssoft.locationtracking.permission
2
+
3
+ import android.app.Activity
4
+ import com.brickssoft.locationtracking.config.BackgroundPermissionRationale
5
+
6
+ /** JS `PermissionType`; [alias] is the `@CapacitorPlugin` permission alias. */
7
+ enum class PermissionType(val alias: String) {
8
+ LOCATION("location"),
9
+ BACKGROUND_LOCATION("backgroundLocation"),
10
+ ACTIVITY_RECOGNITION("activityRecognition"),
11
+ NOTIFICATIONS("notifications"),
12
+ ;
13
+
14
+ companion object {
15
+ fun fromAlias(alias: String?): PermissionType? = entries.firstOrNull { it.alias == alias }
16
+ }
17
+ }
18
+
19
+ /** Capacitor `PermissionState`; [js] is the JS value. */
20
+ enum class PermissionState(val js: String) {
21
+ GRANTED("granted"),
22
+ DENIED("denied"),
23
+ PROMPT("prompt"),
24
+ PROMPT_WITH_RATIONALE("prompt-with-rationale"),
25
+ }
26
+
27
+ /** Implemented by the bridge (U2) on top of the Capacitor plugin. */
28
+ interface PermissionHost {
29
+ val activity: Activity?
30
+
31
+ /** Wraps `requestPermissionForAliases` + `@PermissionCallback`; [onDone] runs on the main thread. */
32
+ fun requestAliases(aliases: List<String>, onDone: () -> Unit)
33
+ }
34
+
35
+ interface PermissionManager {
36
+ /** API-level aware: background < 29 = foreground; activity recognition < 29 granted; notifications < 33 granted. */
37
+ fun status(): Map<PermissionType, PermissionState>
38
+
39
+ fun hasForegroundLocation(): Boolean
40
+
41
+ fun hasBackgroundLocation(): Boolean
42
+
43
+ fun hasActivityRecognition(): Boolean
44
+
45
+ fun hasNotifications(): Boolean
46
+
47
+ /** Main thread. Background location is always requested last. */
48
+ fun request(
49
+ host: PermissionHost,
50
+ types: List<PermissionType>,
51
+ rationale: BackgroundPermissionRationale,
52
+ onDone: (Map<PermissionType, PermissionState>) -> Unit,
53
+ )
54
+ }