@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.
- package/LICENSE +21 -0
- package/README.md +2759 -2
- package/android/build.gradle +106 -0
- package/android/consumer-rules.pro +42 -0
- package/android/src/main/AndroidManifest.xml +52 -0
- package/android/src/main/java/com/brickssoft/locationtracking/LocationTrackingPlugin.kt +431 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingListener.kt +52 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingNative.kt +267 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/NativeListeners.kt +270 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/NativeThread.kt +57 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/BridgeServices.kt +64 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/CallResponder.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +281 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +541 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginPermissionHost.kt +76 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/WatchRegistry.kt +84 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +231 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +574 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +170 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +73 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +45 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/AppDispatchers.kt +21 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Clock.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +217 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Constants.kt +78 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Errors.kt +27 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/EventBus.kt +39 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +63 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Iso8601.kt +63 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Logger.kt +68 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/RecordHooks.kt +47 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +64 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqlSupport.kt +79 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteGeofenceStore.kt +144 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteLocationStore.kt +215 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/Stores.kt +45 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +111 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +104 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceMonitor.kt +335 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceMonitor.kt +36 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceStateMapping.kt +69 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +1281 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/ElasticDistance.kt +68 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/LocationRequests.kt +74 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/MotionStateMachine.kt +291 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/StationaryRegion.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +60 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +90 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/DefaultGeofenceManager.kt +899 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/DwellTracker.kt +80 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/GeofenceManager.kt +33 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/PolygonMath.kt +194 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +713 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +69 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarms.kt +100 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatScheduler.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatWindow.kt +179 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +213 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/BodyBuilder.kt +73 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +118 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +18 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +551 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/StrictJson.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +109 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +143 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +461 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFileProvider.kt +9 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFormatter.kt +148 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogStore.kt +30 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +176 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/GeofenceSpec.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +550 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +106 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +53 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/BackgroundRationaleDialog.kt +85 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt +333 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/PermissionManager.kt +54 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +277 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionService.kt +39 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt +93 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultOdometer.kt +99 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +166 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/Geo.kt +56 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/KalmanFilter.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/Processing.kt +48 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +187 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +169 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/ProviderPackaging.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidActivityBackend.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceBackend.kt +181 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceReceiver.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +265 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidProviderBundle.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +98 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt +80 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt +62 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsErrors.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt +152 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt +78 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +176 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +62 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt +88 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsTasks.kt +55 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt +71 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt +57 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsErrors.kt +76 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt +116 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt +67 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsIntents.kt +83 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +197 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +55 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsTasks.kt +44 -0
- package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +95 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +44 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootReceiver.kt +175 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/DefaultServiceController.kt +196 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt +319 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt +42 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationFactory.kt +338 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceCommands.kt +204 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceController.kt +22 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceDeps.kt +61 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/WhileInUseStartCheck.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/DefaultDeviceSettings.kt +79 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/DeviceSettings.kt +21 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/OemPowerManagers.kt +198 -0
- package/android/src/main/res/drawable/lt_ic_notification.xml +11 -0
- package/android/src/main/res/values/lt_strings.xml +11 -0
- package/android/src/main/res/xml/lt_file_paths.xml +5 -0
- package/dist/esm/definitions.d.ts +719 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/events.d.ts +50 -0
- package/dist/esm/events.js +52 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/plugin.d.ts +3 -0
- package/dist/esm/plugin.js +10 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/esm/web/config.d.ts +19 -0
- package/dist/esm/web/config.js +243 -0
- package/dist/esm/web/config.js.map +1 -0
- package/dist/esm/web/device.d.ts +9 -0
- package/dist/esm/web/device.js +90 -0
- package/dist/esm/web/device.js.map +1 -0
- package/dist/esm/web/errors.d.ts +14 -0
- package/dist/esm/web/errors.js +19 -0
- package/dist/esm/web/errors.js.map +1 -0
- package/dist/esm/web/geolocation.d.ts +26 -0
- package/dist/esm/web/geolocation.js +117 -0
- package/dist/esm/web/geolocation.js.map +1 -0
- package/dist/esm/web/location.d.ts +7 -0
- package/dist/esm/web/location.js +70 -0
- package/dist/esm/web/location.js.map +1 -0
- package/dist/esm/web.d.ts +164 -0
- package/dist/esm/web.js +328 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +945 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +948 -0
- package/dist/plugin.js.map +1 -0
- 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
|
+
}
|
package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt
ADDED
|
@@ -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
|
package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt
ADDED
|
@@ -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
|
+
}
|