react-native-nitro-location-tracking 0.1.17 → 0.1.19
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.
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
10
10
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
11
11
|
|
|
12
|
+
<!-- Required on Android 11+ (API 30) to query all installed packages
|
|
13
|
+
for mock location detection via AppOpsManager -->
|
|
14
|
+
<queries>
|
|
15
|
+
<intent>
|
|
16
|
+
<action android:name="android.intent.action.MAIN" />
|
|
17
|
+
</intent>
|
|
18
|
+
</queries>
|
|
19
|
+
|
|
12
20
|
<application>
|
|
13
21
|
<service
|
|
14
22
|
android:name=".LocationForegroundService"
|
|
@@ -21,12 +21,14 @@ class LocationEngine(private val context: Context) {
|
|
|
21
21
|
|
|
22
22
|
var onLocation: ((LocationData) -> Unit)? = null
|
|
23
23
|
var onMotionChange: ((Boolean) -> Unit)? = null
|
|
24
|
+
var onMockLocationChanged: ((Boolean) -> Unit)? = null
|
|
24
25
|
var dbWriter: NativeDBWriter? = null
|
|
25
26
|
// var currentRideId: String? = null
|
|
26
27
|
var rejectMockLocations: Boolean = false
|
|
27
28
|
val speedMonitor = SpeedMonitor()
|
|
28
29
|
val tripCalculator = TripCalculator()
|
|
29
30
|
private var lastSpeed = 0f
|
|
31
|
+
private var lastMockState: Boolean? = null
|
|
30
32
|
private var tracking = false
|
|
31
33
|
var lastLocation: Location? = null
|
|
32
34
|
private set
|
|
@@ -116,8 +118,16 @@ class LocationEngine(private val context: Context) {
|
|
|
116
118
|
lastLocation = location
|
|
117
119
|
val data = locationToData(location)
|
|
118
120
|
|
|
121
|
+
// Check mock location state change and notify
|
|
122
|
+
val isMock = data.isMockLocation == true
|
|
123
|
+
if (isMock != lastMockState) {
|
|
124
|
+
lastMockState = isMock
|
|
125
|
+
Log.d(TAG, "Mock location state changed: $isMock")
|
|
126
|
+
onMockLocationChanged?.invoke(isMock)
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
// Skip mock locations if rejection is enabled
|
|
120
|
-
if (rejectMockLocations &&
|
|
130
|
+
if (rejectMockLocations && isMock) {
|
|
121
131
|
Log.d(TAG, "Rejecting mock location")
|
|
122
132
|
return
|
|
123
133
|
}
|
|
@@ -164,18 +174,31 @@ class LocationEngine(private val context: Context) {
|
|
|
164
174
|
return mockSetting == "1"
|
|
165
175
|
}
|
|
166
176
|
|
|
167
|
-
// API 23+:
|
|
177
|
+
// API 23+: scan all installed packages for OP_MOCK_LOCATION permission
|
|
168
178
|
try {
|
|
169
179
|
val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
|
170
|
-
|
|
171
|
-
val
|
|
172
|
-
val method = AppOpsManager::class.java.getMethod(
|
|
180
|
+
val opMockLocation = 58 // OP_MOCK_LOCATION
|
|
181
|
+
val checkOp = AppOpsManager::class.java.getMethod(
|
|
173
182
|
"checkOp", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType, String::class.java
|
|
174
183
|
)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
178
|
-
|
|
184
|
+
|
|
185
|
+
val pm = context.packageManager
|
|
186
|
+
@Suppress("DEPRECATION")
|
|
187
|
+
val packages = pm.getInstalledApplications(0)
|
|
188
|
+
for (appInfo in packages) {
|
|
189
|
+
if (appInfo.packageName == context.packageName) continue
|
|
190
|
+
try {
|
|
191
|
+
val result = checkOp.invoke(
|
|
192
|
+
appOps, opMockLocation, appInfo.uid, appInfo.packageName
|
|
193
|
+
) as Int
|
|
194
|
+
if (result == AppOpsManager.MODE_ALLOWED) {
|
|
195
|
+
Log.d(TAG, "Mock location provider detected: ${appInfo.packageName}")
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
} catch (_: Exception) {
|
|
199
|
+
// Some packages may not be queryable
|
|
200
|
+
}
|
|
201
|
+
}
|
|
179
202
|
} catch (e: Exception) {
|
|
180
203
|
Log.w(TAG, "Could not check mock location app ops: ${e.message}")
|
|
181
204
|
}
|
package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/MockLocationMonitor.kt
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
package com.margelo.nitro.nitrolocationtracking
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
-
import android.app.AppOpsManager
|
|
5
4
|
import android.content.Context
|
|
5
|
+
import android.location.Location
|
|
6
6
|
import android.os.Build
|
|
7
7
|
import android.os.Handler
|
|
8
8
|
import android.os.Looper
|
|
9
9
|
import android.provider.Settings
|
|
10
10
|
import android.util.Log
|
|
11
|
+
import com.google.android.gms.location.CurrentLocationRequest
|
|
12
|
+
import com.google.android.gms.location.LocationServices
|
|
13
|
+
import com.google.android.gms.location.Priority
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
|
-
* Periodically
|
|
14
|
-
*
|
|
15
|
-
* mock location apps (e.g. Fake GPS, Mock Locations) at the system level.
|
|
16
|
+
* Periodically checks whether mock/fake GPS is actively being used on the device.
|
|
17
|
+
* Works independently of location tracking — can be used on login/verify screens.
|
|
16
18
|
*
|
|
17
|
-
*
|
|
19
|
+
* Detection strategy:
|
|
18
20
|
* - Pre-API 23: Settings.Secure.ALLOW_MOCK_LOCATION
|
|
19
|
-
* - API 23+:
|
|
20
|
-
*
|
|
21
|
+
* - API 23+: Requests a single location via FusedLocationProviderClient and checks
|
|
22
|
+
* Location.isMock / isFromMockProvider. This is the only reliable way to know
|
|
23
|
+
* if mock location is currently ACTIVE (not just if a mock app is installed).
|
|
21
24
|
*
|
|
22
25
|
* The callback fires only when the state changes (deduplicated).
|
|
23
26
|
*/
|
|
@@ -25,11 +28,11 @@ class MockLocationMonitor(private val context: Context) {
|
|
|
25
28
|
|
|
26
29
|
companion object {
|
|
27
30
|
private const val TAG = "MockLocationMonitor"
|
|
28
|
-
/** Default poll interval in milliseconds */
|
|
29
31
|
private const val DEFAULT_POLL_INTERVAL_MS = 3000L
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
35
|
+
private val fusedClient = LocationServices.getFusedLocationProviderClient(context)
|
|
33
36
|
private var callback: ((Boolean) -> Unit)? = null
|
|
34
37
|
private var lastState: Boolean? = null
|
|
35
38
|
private var polling = false
|
|
@@ -38,18 +41,15 @@ class MockLocationMonitor(private val context: Context) {
|
|
|
38
41
|
private val pollRunnable = object : Runnable {
|
|
39
42
|
override fun run() {
|
|
40
43
|
if (!polling) return
|
|
41
|
-
|
|
44
|
+
checkMockLocation()
|
|
42
45
|
mainHandler.postDelayed(this, pollIntervalMs)
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
fun setCallback(callback: (Boolean) -> Unit) {
|
|
47
50
|
this.callback = callback
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
lastState = current
|
|
51
|
-
callback.invoke(current)
|
|
52
|
-
// Start polling
|
|
51
|
+
// Kick off an immediate check
|
|
52
|
+
checkMockLocation()
|
|
53
53
|
startPolling()
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -66,75 +66,66 @@ class MockLocationMonitor(private val context: Context) {
|
|
|
66
66
|
Log.d(TAG, "Mock location polling stopped")
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
private fun
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
callback?.invoke(current)
|
|
69
|
+
private fun notifyIfChanged(isMock: Boolean) {
|
|
70
|
+
if (isMock != lastState) {
|
|
71
|
+
lastState = isMock
|
|
72
|
+
Log.d(TAG, "Mock location state changed: isMockEnabled=$isMock")
|
|
73
|
+
callback?.invoke(isMock)
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@SuppressLint("DiscouragedPrivateApi")
|
|
82
|
-
fun isMockLocationActive(): Boolean {
|
|
83
|
-
// Pre-API 23: check the global mock location setting
|
|
77
|
+
@SuppressLint("MissingPermission")
|
|
78
|
+
private fun checkMockLocation() {
|
|
79
|
+
// Pre-API 23: use the global settings flag
|
|
84
80
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
|
85
81
|
@Suppress("DEPRECATION")
|
|
86
82
|
val mockSetting = Settings.Secure.getString(
|
|
87
83
|
context.contentResolver,
|
|
88
84
|
Settings.Secure.ALLOW_MOCK_LOCATION
|
|
89
85
|
)
|
|
90
|
-
|
|
86
|
+
notifyIfChanged(mockSetting == "1")
|
|
87
|
+
return
|
|
91
88
|
}
|
|
92
89
|
|
|
93
|
-
// API 23+:
|
|
90
|
+
// API 23+: request a current location and check isMock flag
|
|
94
91
|
try {
|
|
95
|
-
val
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Int::class.javaPrimitiveType,
|
|
100
|
-
Int::class.javaPrimitiveType,
|
|
101
|
-
String::class.java
|
|
102
|
-
)
|
|
103
|
-
val result = method.invoke(
|
|
104
|
-
appOps, opMockLocation, android.os.Process.myUid(), context.packageName
|
|
105
|
-
) as Int
|
|
106
|
-
if (result == AppOpsManager.MODE_ALLOWED) return true
|
|
107
|
-
} catch (e: Exception) {
|
|
108
|
-
Log.w(TAG, "Could not check mock location app ops: ${e.message}")
|
|
109
|
-
}
|
|
92
|
+
val request = CurrentLocationRequest.Builder()
|
|
93
|
+
.setPriority(Priority.PRIORITY_HIGH_ACCURACY)
|
|
94
|
+
.setMaxUpdateAgeMillis(5000)
|
|
95
|
+
.build()
|
|
110
96
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
pm.getApplicationInfo(pkg, 0)
|
|
127
|
-
Log.d(TAG, "Known mock location app detected: $pkg")
|
|
128
|
-
return true
|
|
129
|
-
} catch (_: Exception) {
|
|
130
|
-
// Not installed
|
|
97
|
+
fusedClient.getCurrentLocation(request, null)
|
|
98
|
+
.addOnSuccessListener { location: Location? ->
|
|
99
|
+
if (location != null) {
|
|
100
|
+
val isMock = isLocationMock(location)
|
|
101
|
+
mainHandler.post { notifyIfChanged(isMock) }
|
|
102
|
+
} else {
|
|
103
|
+
// No location available — try last location as fallback
|
|
104
|
+
fusedClient.lastLocation.addOnSuccessListener { lastLoc: Location? ->
|
|
105
|
+
if (lastLoc != null) {
|
|
106
|
+
val isMock = isLocationMock(lastLoc)
|
|
107
|
+
mainHandler.post { notifyIfChanged(isMock) }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
131
111
|
}
|
|
132
|
-
|
|
112
|
+
.addOnFailureListener { e ->
|
|
113
|
+
Log.w(TAG, "Failed to get current location for mock check: ${e.message}")
|
|
114
|
+
}
|
|
115
|
+
} catch (e: SecurityException) {
|
|
116
|
+
Log.w(TAG, "No location permission for mock check: ${e.message}")
|
|
133
117
|
} catch (e: Exception) {
|
|
134
|
-
Log.w(TAG, "
|
|
118
|
+
Log.w(TAG, "Error checking mock location: ${e.message}")
|
|
135
119
|
}
|
|
120
|
+
}
|
|
136
121
|
|
|
137
|
-
|
|
122
|
+
private fun isLocationMock(location: Location): Boolean {
|
|
123
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
124
|
+
location.isMock
|
|
125
|
+
} else {
|
|
126
|
+
@Suppress("DEPRECATION")
|
|
127
|
+
location.isFromMockProvider
|
|
128
|
+
}
|
|
138
129
|
}
|
|
139
130
|
|
|
140
131
|
fun destroy() {
|
package/package.json
CHANGED