react-native-nitro-compass 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NitroCompass.podspec +31 -0
- package/README.md +206 -0
- package/android/CMakeLists.txt +32 -0
- package/android/build.gradle +148 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +9 -0
- package/android/src/main/java/com/margelo/nitro/nitrocompass/HybridNitroCompass.kt +481 -0
- package/android/src/main/java/com/margelo/nitro/nitrocompass/NitroCompassPackage.kt +18 -0
- package/app.plugin.js +16 -0
- package/ios/Bridge.h +8 -0
- package/ios/HybridNitroCompass.swift +473 -0
- package/lib/commonjs/hook.js +69 -0
- package/lib/commonjs/hook.js.map +1 -0
- package/lib/commonjs/index.js +39 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/multiplex.js +109 -0
- package/lib/commonjs/multiplex.js.map +1 -0
- package/lib/commonjs/native.js +9 -0
- package/lib/commonjs/native.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/NitroCompass.nitro.js +6 -0
- package/lib/commonjs/specs/NitroCompass.nitro.js.map +1 -0
- package/lib/module/hook.js +65 -0
- package/lib/module/hook.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/multiplex.js +103 -0
- package/lib/module/multiplex.js.map +1 -0
- package/lib/module/native.js +5 -0
- package/lib/module/native.js.map +1 -0
- package/lib/module/specs/NitroCompass.nitro.js +4 -0
- package/lib/module/specs/NitroCompass.nitro.js.map +1 -0
- package/lib/typescript/src/hook.d.ts +49 -0
- package/lib/typescript/src/hook.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +8 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/multiplex.d.ts +38 -0
- package/lib/typescript/src/multiplex.d.ts.map +1 -0
- package/lib/typescript/src/native.d.ts +3 -0
- package/lib/typescript/src/native.d.ts.map +1 -0
- package/lib/typescript/src/specs/NitroCompass.nitro.d.ts +176 -0
- package/lib/typescript/src/specs/NitroCompass.nitro.d.ts.map +1 -0
- package/nitro.json +30 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroCompass+autolinking.cmake +81 -0
- package/nitrogen/generated/android/NitroCompass+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroCompassOnLoad.cpp +60 -0
- package/nitrogen/generated/android/NitroCompassOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JAccuracyQuality.hpp +64 -0
- package/nitrogen/generated/android/c++/JCompassSample.hpp +61 -0
- package/nitrogen/generated/android/c++/JFunc_void_AccuracyQuality.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_CompassSample.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridNitroCompassSpec.cpp +143 -0
- package/nitrogen/generated/android/c++/JHybridNitroCompassSpec.hpp +75 -0
- package/nitrogen/generated/android/c++/JPermissionStatus.hpp +61 -0
- package/nitrogen/generated/android/c++/JSensorDiagnostics.hpp +58 -0
- package/nitrogen/generated/android/c++/JSensorKind.hpp +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/AccuracyQuality.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/CompassSample.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/Func_void_AccuracyQuality.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/Func_void_CompassSample.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/Func_void_bool.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/HybridNitroCompassSpec.kt +118 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/NitroCompassOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/PermissionStatus.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/SensorDiagnostics.kt +51 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/SensorKind.kt +24 -0
- package/nitrogen/generated/ios/NitroCompass+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroCompass-Swift-Cxx-Bridge.cpp +73 -0
- package/nitrogen/generated/ios/NitroCompass-Swift-Cxx-Bridge.hpp +267 -0
- package/nitrogen/generated/ios/NitroCompass-Swift-Cxx-Umbrella.hpp +61 -0
- package/nitrogen/generated/ios/NitroCompassAutolinking.mm +33 -0
- package/nitrogen/generated/ios/NitroCompassAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridNitroCompassSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroCompassSpecSwift.hpp +180 -0
- package/nitrogen/generated/ios/swift/AccuracyQuality.swift +48 -0
- package/nitrogen/generated/ios/swift/CompassSample.swift +34 -0
- package/nitrogen/generated/ios/swift/Func_void_AccuracyQuality.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_CompassSample.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_PermissionStatus.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridNitroCompassSpec.swift +67 -0
- package/nitrogen/generated/ios/swift/HybridNitroCompassSpec_cxx.swift +309 -0
- package/nitrogen/generated/ios/swift/PermissionStatus.swift +44 -0
- package/nitrogen/generated/ios/swift/SensorDiagnostics.swift +29 -0
- package/nitrogen/generated/ios/swift/SensorKind.swift +44 -0
- package/nitrogen/generated/shared/c++/AccuracyQuality.hpp +84 -0
- package/nitrogen/generated/shared/c++/CompassSample.hpp +87 -0
- package/nitrogen/generated/shared/c++/HybridNitroCompassSpec.cpp +33 -0
- package/nitrogen/generated/shared/c++/HybridNitroCompassSpec.hpp +87 -0
- package/nitrogen/generated/shared/c++/PermissionStatus.hpp +80 -0
- package/nitrogen/generated/shared/c++/SensorDiagnostics.hpp +84 -0
- package/nitrogen/generated/shared/c++/SensorKind.hpp +80 -0
- package/package.json +136 -0
- package/react-native.config.js +11 -0
- package/src/hook.ts +118 -0
- package/src/index.ts +28 -0
- package/src/multiplex.ts +117 -0
- package/src/native.ts +5 -0
- package/src/specs/NitroCompass.nitro.ts +193 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrocompass
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.app.ActivityManager
|
|
5
|
+
import android.app.Application
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.hardware.Sensor
|
|
8
|
+
import android.hardware.SensorEvent
|
|
9
|
+
import android.hardware.SensorEventListener
|
|
10
|
+
import android.hardware.SensorManager
|
|
11
|
+
import android.hardware.display.DisplayManager
|
|
12
|
+
import android.os.Build
|
|
13
|
+
import android.os.Bundle
|
|
14
|
+
import android.os.Handler
|
|
15
|
+
import android.os.HandlerThread
|
|
16
|
+
import android.os.Looper
|
|
17
|
+
import android.os.SystemClock
|
|
18
|
+
import android.view.Display
|
|
19
|
+
import android.view.Surface
|
|
20
|
+
import java.lang.ref.WeakReference
|
|
21
|
+
import androidx.annotation.Keep
|
|
22
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
23
|
+
import com.margelo.nitro.NitroModules
|
|
24
|
+
import com.margelo.nitro.core.Promise
|
|
25
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
26
|
+
import kotlin.math.abs
|
|
27
|
+
import kotlin.math.sqrt
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Android implementation of NitroCompass.
|
|
31
|
+
*
|
|
32
|
+
* Uses Sensor.TYPE_ROTATION_VECTOR (gyro+accel+mag fused) with a
|
|
33
|
+
* TYPE_GEOMAGNETIC_ROTATION_VECTOR fallback for gyroless / budget devices.
|
|
34
|
+
* Sensor delivery happens on a dedicated HandlerThread so it never blocks
|
|
35
|
+
* the UI thread; samples are forwarded to the JS callback directly.
|
|
36
|
+
*
|
|
37
|
+
* The math is adapted from the MIT-licensed Andromeda library that powers
|
|
38
|
+
* the Trail Sense app: https://github.com/kylecorry31/andromeda
|
|
39
|
+
*/
|
|
40
|
+
@DoNotStrip
|
|
41
|
+
@Keep
|
|
42
|
+
class HybridNitroCompass : HybridNitroCompassSpec() {
|
|
43
|
+
|
|
44
|
+
companion object {
|
|
45
|
+
// Some Android sensor stacks (notably certain Samsung/Huawei builds)
|
|
46
|
+
// can silently stall after a screen off / sensor pressure event.
|
|
47
|
+
// The rotation-vector sensor at SENSOR_DELAY_GAME nominally fires
|
|
48
|
+
// every ~20ms; if no event has arrived in 1.5s we assume the stack
|
|
49
|
+
// froze and force a re-registration.
|
|
50
|
+
private const val WATCHDOG_PERIOD_MS = 1_500L
|
|
51
|
+
private const val STALE_THRESHOLD_NS = 1_500_000_000L
|
|
52
|
+
|
|
53
|
+
// Earth's magnetic field magnitude is typically 25–65 µT. Anything
|
|
54
|
+
// outside this band (with a small grace margin) is treated as
|
|
55
|
+
// external interference — laptops, monitors, car engines, and
|
|
56
|
+
// structural steel routinely push readings well above 100 µT.
|
|
57
|
+
private const val EARTH_FIELD_MIN_UT = 20.0
|
|
58
|
+
private const val EARTH_FIELD_MAX_UT = 70.0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Volatile private var filterDeg: Double = 1.0
|
|
62
|
+
@Volatile private var lastEmittedHeading: Double = Double.NaN
|
|
63
|
+
@Volatile private var lastAccuracyDeg: Double = -1.0
|
|
64
|
+
@Volatile private var lastSample: CompassSample? = null
|
|
65
|
+
@Volatile private var lastQuality: AccuracyQuality? = null
|
|
66
|
+
@Volatile private var declinationDeg: Double = 0.0
|
|
67
|
+
@Volatile private var pauseOnBackground: Boolean = true
|
|
68
|
+
@Volatile private var started: Boolean = false
|
|
69
|
+
@Volatile private var isSubscribed: Boolean = false
|
|
70
|
+
@Volatile private var lastEventNs: Long = 0L
|
|
71
|
+
@Volatile private var lastInterference: Boolean? = null
|
|
72
|
+
@Volatile private var currentActivityRef: WeakReference<Activity>? = null
|
|
73
|
+
|
|
74
|
+
private val rotationMatrix = FloatArray(16)
|
|
75
|
+
private val remappedMatrix = FloatArray(16)
|
|
76
|
+
private val orientation = FloatArray(3)
|
|
77
|
+
|
|
78
|
+
private val epoch = AtomicInteger(0)
|
|
79
|
+
private val activityCounter = AtomicInteger(0)
|
|
80
|
+
private var sensorThread: HandlerThread? = null
|
|
81
|
+
private var sensorHandler: Handler? = null
|
|
82
|
+
private var activeSensor: Sensor? = null
|
|
83
|
+
private var activeListener: SensorEventListener? = null
|
|
84
|
+
private var lifecycleCallbacks: Application.ActivityLifecycleCallbacks? = null
|
|
85
|
+
private var onHeading: ((CompassSample) -> Unit)? = null
|
|
86
|
+
private var calibrationCb: ((AccuracyQuality) -> Unit)? = null
|
|
87
|
+
private var interferenceCb: ((Boolean) -> Unit)? = null
|
|
88
|
+
|
|
89
|
+
private val watchdogHandler = Handler(Looper.getMainLooper())
|
|
90
|
+
private val watchdogRunnable = object : Runnable {
|
|
91
|
+
override fun run() {
|
|
92
|
+
// When the app is backgrounded, the OS legitimately suspends or
|
|
93
|
+
// throttles non-wake-up sensors (Doze on API 23+, background limits
|
|
94
|
+
// on API 26+). Re-registering the listener won't change that — it
|
|
95
|
+
// just burns power flapping every 1.5s. Skip the staleness check
|
|
96
|
+
// and re-arm; we'll resume normal watchdog behaviour on foreground.
|
|
97
|
+
val backgrounded = activityCounter.get() == 0
|
|
98
|
+
val last = lastEventNs
|
|
99
|
+
val now = SystemClock.elapsedRealtimeNanos()
|
|
100
|
+
if (!backgrounded && last > 0L && now - last > STALE_THRESHOLD_NS) {
|
|
101
|
+
synchronized(this@HybridNitroCompass) {
|
|
102
|
+
if (started && isSubscribed) {
|
|
103
|
+
unsubscribeLocked()
|
|
104
|
+
subscribeLocked()
|
|
105
|
+
// subscribeLocked() re-arms the watchdog itself, so don't
|
|
106
|
+
// double-post below. Reset the timestamp to give the fresh
|
|
107
|
+
// subscription a full window before being judged stale.
|
|
108
|
+
lastEventNs = SystemClock.elapsedRealtimeNanos()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if (started && isSubscribed) {
|
|
114
|
+
watchdogHandler.postDelayed(this, WATCHDOG_PERIOD_MS)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private val context: Context
|
|
120
|
+
get() = NitroModules.applicationContext
|
|
121
|
+
?: throw IllegalStateException("NitroModules.applicationContext is null — was Nitro installed?")
|
|
122
|
+
|
|
123
|
+
override fun start(filterDegrees: Double, onHeading: (sample: CompassSample) -> Unit) {
|
|
124
|
+
synchronized(this) {
|
|
125
|
+
stopLocked()
|
|
126
|
+
started = true
|
|
127
|
+
filterDeg = filterDegrees.coerceAtLeast(0.0)
|
|
128
|
+
this.onHeading = onHeading
|
|
129
|
+
lastEmittedHeading = Double.NaN
|
|
130
|
+
lastAccuracyDeg = -1.0
|
|
131
|
+
lastSample = null
|
|
132
|
+
lastQuality = null
|
|
133
|
+
|
|
134
|
+
registerLifecycleCallbacks()
|
|
135
|
+
subscribeLocked()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
override fun stop() {
|
|
140
|
+
synchronized(this) {
|
|
141
|
+
stopLocked()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
override fun hasCompass(): Boolean {
|
|
146
|
+
val sm = NitroModules.applicationContext?.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
|
|
147
|
+
?: return false
|
|
148
|
+
return sm.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) != null ||
|
|
149
|
+
sm.getDefaultSensor(Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR) != null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
override fun isStarted(): Boolean = started
|
|
153
|
+
|
|
154
|
+
override fun setFilter(degrees: Double) {
|
|
155
|
+
filterDeg = degrees.coerceAtLeast(0.0)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
override fun getDiagnostics(): SensorDiagnostics? {
|
|
159
|
+
val sm = NitroModules.applicationContext?.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
|
|
160
|
+
?: return null
|
|
161
|
+
return when {
|
|
162
|
+
sm.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) != null ->
|
|
163
|
+
SensorDiagnostics(SensorKind.ROTATIONVECTOR)
|
|
164
|
+
sm.getDefaultSensor(Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR) != null ->
|
|
165
|
+
SensorDiagnostics(SensorKind.GEOMAGNETICROTATIONVECTOR)
|
|
166
|
+
else -> null
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
override fun getCurrentHeading(): CompassSample? = lastSample
|
|
171
|
+
|
|
172
|
+
override fun setDeclination(degrees: Double) {
|
|
173
|
+
declinationDeg = degrees
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
override fun setOnCalibrationNeeded(onChange: (quality: AccuracyQuality) -> Unit) {
|
|
177
|
+
calibrationCb = onChange
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
override fun setOnInterferenceDetected(onChange: (interferenceDetected: Boolean) -> Unit) {
|
|
181
|
+
interferenceCb = onChange
|
|
182
|
+
// Replay the current state so a late-registering consumer sees the
|
|
183
|
+
// truth instead of waiting for the next transition (which may never
|
|
184
|
+
// arrive if the field stays stable).
|
|
185
|
+
lastInterference?.let(onChange)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
override fun setPauseOnBackground(enabled: Boolean) {
|
|
189
|
+
synchronized(this) {
|
|
190
|
+
pauseOnBackground = enabled
|
|
191
|
+
if (enabled && started && isSubscribed && activityCounter.get() == 0) {
|
|
192
|
+
unsubscribeLocked()
|
|
193
|
+
} else if (!enabled && started && !isSubscribed) {
|
|
194
|
+
subscribeLocked()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Sensors don't require a runtime permission on Android, so both
|
|
200
|
+
// permission methods are unconditionally granted.
|
|
201
|
+
override fun getPermissionStatus(): PermissionStatus = PermissionStatus.GRANTED
|
|
202
|
+
|
|
203
|
+
override fun requestPermission(): Promise<PermissionStatus> {
|
|
204
|
+
val p = Promise<PermissionStatus>()
|
|
205
|
+
p.resolve(PermissionStatus.GRANTED)
|
|
206
|
+
return p
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private fun stopLocked() {
|
|
210
|
+
started = false
|
|
211
|
+
unsubscribeLocked()
|
|
212
|
+
unregisterLifecycleCallbacks()
|
|
213
|
+
onHeading = null
|
|
214
|
+
lastSample = null
|
|
215
|
+
lastQuality = null
|
|
216
|
+
lastInterference = null
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private fun subscribeLocked() {
|
|
220
|
+
if (isSubscribed) return
|
|
221
|
+
val sm = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
|
|
222
|
+
?: throw IllegalStateException("SensorManager unavailable")
|
|
223
|
+
val sensor = sm.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
|
|
224
|
+
?: sm.getDefaultSensor(Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR)
|
|
225
|
+
?: throw IllegalStateException("No rotation sensor on this device")
|
|
226
|
+
|
|
227
|
+
val myEpoch = epoch.incrementAndGet()
|
|
228
|
+
val thread = HandlerThread("NitroCompass-Sensor").also { it.start() }
|
|
229
|
+
val handler = Handler(thread.looper)
|
|
230
|
+
val listener = object : SensorEventListener {
|
|
231
|
+
override fun onSensorChanged(event: SensorEvent) {
|
|
232
|
+
if (myEpoch != epoch.get()) return
|
|
233
|
+
handleSensorEvent(event)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
|
|
237
|
+
if (myEpoch != epoch.get()) return
|
|
238
|
+
handleAccuracyChanged(accuracy)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
sm.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_GAME, handler)
|
|
242
|
+
|
|
243
|
+
// Optional second subscription for magnetic-interference detection.
|
|
244
|
+
// 5Hz is plenty (we only care about transitions in/out of the
|
|
245
|
+
// Earth-field band) and keeps power cost negligible. Same listener
|
|
246
|
+
// instance — events are demuxed by sensor type in handleSensorEvent.
|
|
247
|
+
sm.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)?.let { magSensor ->
|
|
248
|
+
sm.registerListener(listener, magSensor, SensorManager.SENSOR_DELAY_NORMAL, handler)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
sensorThread = thread
|
|
252
|
+
sensorHandler = handler
|
|
253
|
+
activeSensor = sensor
|
|
254
|
+
activeListener = listener
|
|
255
|
+
isSubscribed = true
|
|
256
|
+
|
|
257
|
+
lastEventNs = 0L
|
|
258
|
+
watchdogHandler.removeCallbacks(watchdogRunnable)
|
|
259
|
+
watchdogHandler.postDelayed(watchdogRunnable, WATCHDOG_PERIOD_MS)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private fun unsubscribeLocked() {
|
|
263
|
+
watchdogHandler.removeCallbacks(watchdogRunnable)
|
|
264
|
+
if (!isSubscribed) {
|
|
265
|
+
sensorThread?.quitSafely()
|
|
266
|
+
sensorThread = null
|
|
267
|
+
sensorHandler = null
|
|
268
|
+
activeSensor = null
|
|
269
|
+
activeListener = null
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
epoch.incrementAndGet()
|
|
273
|
+
val sm = NitroModules.applicationContext?.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
|
|
274
|
+
activeListener?.let { sm?.unregisterListener(it) }
|
|
275
|
+
activeListener = null
|
|
276
|
+
activeSensor = null
|
|
277
|
+
sensorHandler = null
|
|
278
|
+
sensorThread?.quitSafely()
|
|
279
|
+
sensorThread = null
|
|
280
|
+
isSubscribed = false
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private fun registerLifecycleCallbacks() {
|
|
284
|
+
if (lifecycleCallbacks != null) return
|
|
285
|
+
val app = NitroModules.applicationContext?.applicationContext as? Application ?: return
|
|
286
|
+
// start() can be called from a headless / background context (e.g. a
|
|
287
|
+
// headless JS task, or before any Activity has come up). Don't assume
|
|
288
|
+
// foreground — query the OS so pauseOnBackground=true actually keeps
|
|
289
|
+
// the sensor unsubscribed when the app isn't user-visible.
|
|
290
|
+
activityCounter.set(if (isAppInForeground(app)) 1 else 0)
|
|
291
|
+
val cb = object : Application.ActivityLifecycleCallbacks {
|
|
292
|
+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
|
293
|
+
captureActivity(activity)
|
|
294
|
+
}
|
|
295
|
+
override fun onActivityStarted(activity: Activity) {
|
|
296
|
+
captureActivity(activity)
|
|
297
|
+
if (activityCounter.getAndIncrement() == 0) handleForeground()
|
|
298
|
+
}
|
|
299
|
+
override fun onActivityResumed(activity: Activity) {
|
|
300
|
+
captureActivity(activity)
|
|
301
|
+
}
|
|
302
|
+
override fun onActivityPaused(activity: Activity) {}
|
|
303
|
+
override fun onActivityStopped(activity: Activity) {
|
|
304
|
+
if (activityCounter.decrementAndGet() == 0) handleBackground()
|
|
305
|
+
}
|
|
306
|
+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
|
307
|
+
override fun onActivityDestroyed(activity: Activity) {
|
|
308
|
+
if (currentActivityRef?.get() == activity) {
|
|
309
|
+
currentActivityRef = null
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
app.registerActivityLifecycleCallbacks(cb)
|
|
314
|
+
lifecycleCallbacks = cb
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private fun unregisterLifecycleCallbacks() {
|
|
318
|
+
val cb = lifecycleCallbacks ?: return
|
|
319
|
+
val app = NitroModules.applicationContext?.applicationContext as? Application
|
|
320
|
+
app?.unregisterActivityLifecycleCallbacks(cb)
|
|
321
|
+
lifecycleCallbacks = null
|
|
322
|
+
activityCounter.set(0)
|
|
323
|
+
currentActivityRef = null
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private fun captureActivity(activity: Activity) {
|
|
327
|
+
currentActivityRef = WeakReference(activity)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private fun isAppInForeground(app: Application): Boolean {
|
|
331
|
+
val am = app.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager ?: return true
|
|
332
|
+
val procs = am.runningAppProcesses ?: return true
|
|
333
|
+
val pid = android.os.Process.myPid()
|
|
334
|
+
for (proc in procs) {
|
|
335
|
+
if (proc.pid == pid) {
|
|
336
|
+
return proc.importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return true
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private fun handleBackground() {
|
|
343
|
+
synchronized(this) {
|
|
344
|
+
if (pauseOnBackground && started && isSubscribed) {
|
|
345
|
+
unsubscribeLocked()
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private fun handleForeground() {
|
|
351
|
+
synchronized(this) {
|
|
352
|
+
if (pauseOnBackground && started && !isSubscribed) {
|
|
353
|
+
subscribeLocked()
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private fun handleSensorEvent(event: SensorEvent) {
|
|
359
|
+
val type = event.sensor.type
|
|
360
|
+
if (type == Sensor.TYPE_MAGNETIC_FIELD) {
|
|
361
|
+
handleMagneticEvent(event)
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
if (type != Sensor.TYPE_ROTATION_VECTOR &&
|
|
365
|
+
type != Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR
|
|
366
|
+
) return
|
|
367
|
+
|
|
368
|
+
lastEventNs = SystemClock.elapsedRealtimeNanos()
|
|
369
|
+
|
|
370
|
+
SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
|
|
371
|
+
|
|
372
|
+
val (axisX, axisY) = when (currentSurfaceRotation()) {
|
|
373
|
+
Surface.ROTATION_90 -> SensorManager.AXIS_Y to SensorManager.AXIS_MINUS_X
|
|
374
|
+
Surface.ROTATION_180 -> SensorManager.AXIS_MINUS_X to SensorManager.AXIS_MINUS_Y
|
|
375
|
+
Surface.ROTATION_270 -> SensorManager.AXIS_MINUS_Y to SensorManager.AXIS_X
|
|
376
|
+
else -> SensorManager.AXIS_X to SensorManager.AXIS_Y
|
|
377
|
+
}
|
|
378
|
+
SensorManager.remapCoordinateSystem(rotationMatrix, axisX, axisY, remappedMatrix)
|
|
379
|
+
SensorManager.getOrientation(remappedMatrix, orientation)
|
|
380
|
+
|
|
381
|
+
var heading = Math.toDegrees(orientation[0].toDouble())
|
|
382
|
+
if (heading < 0.0) heading += 360.0
|
|
383
|
+
|
|
384
|
+
if (event.values.size > 4 && event.values[4] >= 0f) {
|
|
385
|
+
val acc = Math.toDegrees(event.values[4].toDouble())
|
|
386
|
+
lastAccuracyDeg = acc
|
|
387
|
+
fireCalibration(qualityFor(acc))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
val prev = lastEmittedHeading
|
|
391
|
+
val delta = if (prev.isNaN()) Double.MAX_VALUE else shortestArc(prev, heading)
|
|
392
|
+
if (filterDeg > 0.0 && delta < filterDeg) return
|
|
393
|
+
lastEmittedHeading = heading
|
|
394
|
+
|
|
395
|
+
var emitted = heading + declinationDeg
|
|
396
|
+
emitted = ((emitted % 360.0) + 360.0) % 360.0
|
|
397
|
+
val sample = CompassSample(emitted, lastAccuracyDeg)
|
|
398
|
+
lastSample = sample
|
|
399
|
+
onHeading?.invoke(sample)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private fun handleMagneticEvent(event: SensorEvent) {
|
|
403
|
+
if (event.values.size < 3) return
|
|
404
|
+
val x = event.values[0]
|
|
405
|
+
val y = event.values[1]
|
|
406
|
+
val z = event.values[2]
|
|
407
|
+
val magnitude = sqrt((x * x + y * y + z * z).toDouble())
|
|
408
|
+
val isInterference = magnitude < EARTH_FIELD_MIN_UT || magnitude > EARTH_FIELD_MAX_UT
|
|
409
|
+
if (lastInterference == isInterference) return
|
|
410
|
+
lastInterference = isInterference
|
|
411
|
+
interferenceCb?.invoke(isInterference)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private fun handleAccuracyChanged(accuracy: Int) {
|
|
415
|
+
val quality = when (accuracy) {
|
|
416
|
+
SensorManager.SENSOR_STATUS_ACCURACY_HIGH -> AccuracyQuality.HIGH
|
|
417
|
+
SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM -> AccuracyQuality.MEDIUM
|
|
418
|
+
SensorManager.SENSOR_STATUS_ACCURACY_LOW -> AccuracyQuality.LOW
|
|
419
|
+
else -> AccuracyQuality.UNRELIABLE
|
|
420
|
+
}
|
|
421
|
+
if (lastAccuracyDeg < 0.0) {
|
|
422
|
+
lastAccuracyDeg = when (quality) {
|
|
423
|
+
AccuracyQuality.HIGH -> 5.0
|
|
424
|
+
AccuracyQuality.MEDIUM -> 15.0
|
|
425
|
+
AccuracyQuality.LOW -> 30.0
|
|
426
|
+
AccuracyQuality.UNRELIABLE -> -1.0
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
fireCalibration(quality)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private fun qualityFor(accuracyDeg: Double): AccuracyQuality {
|
|
433
|
+
return when {
|
|
434
|
+
accuracyDeg < 0 -> AccuracyQuality.UNRELIABLE
|
|
435
|
+
accuracyDeg < 5 -> AccuracyQuality.HIGH
|
|
436
|
+
accuracyDeg < 15 -> AccuracyQuality.MEDIUM
|
|
437
|
+
accuracyDeg < 30 -> AccuracyQuality.LOW
|
|
438
|
+
else -> AccuracyQuality.UNRELIABLE
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private fun fireCalibration(quality: AccuracyQuality) {
|
|
443
|
+
if (quality == lastQuality) return
|
|
444
|
+
lastQuality = quality
|
|
445
|
+
calibrationCb?.invoke(quality)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private fun currentSurfaceRotation(): Int {
|
|
449
|
+
// Prefer the *activity's* display when available — on foldables and
|
|
450
|
+
// multi-window setups the activity's display can differ from the
|
|
451
|
+
// primary display, so reading via DisplayManager.DEFAULT_DISPLAY
|
|
452
|
+
// gives the wrong rotation. Activity.getDisplay() is API 30+;
|
|
453
|
+
// fall back to the deprecated WindowManager.defaultDisplay path on
|
|
454
|
+
// older devices, and to DisplayManager when we have no activity
|
|
455
|
+
// (early in the process before any lifecycle callback has fired).
|
|
456
|
+
val activity = currentActivityRef?.get()
|
|
457
|
+
if (activity != null) {
|
|
458
|
+
val display: Display? = try {
|
|
459
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
460
|
+
activity.display
|
|
461
|
+
} else {
|
|
462
|
+
@Suppress("DEPRECATION")
|
|
463
|
+
activity.windowManager.defaultDisplay
|
|
464
|
+
}
|
|
465
|
+
} catch (_: Throwable) {
|
|
466
|
+
null
|
|
467
|
+
}
|
|
468
|
+
if (display != null) return display.rotation
|
|
469
|
+
}
|
|
470
|
+
val ctx = NitroModules.applicationContext ?: return Surface.ROTATION_0
|
|
471
|
+
val dm = ctx.getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager
|
|
472
|
+
?: return Surface.ROTATION_0
|
|
473
|
+
@Suppress("DEPRECATION")
|
|
474
|
+
return dm.getDisplay(Display.DEFAULT_DISPLAY)?.rotation ?: Surface.ROTATION_0
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private fun shortestArc(from: Double, to: Double): Double {
|
|
478
|
+
val diff = ((to - from + 540.0) % 360.0) - 180.0
|
|
479
|
+
return abs(diff)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrocompass
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
7
|
+
|
|
8
|
+
class NitroCompassPackage : BaseReactPackage() {
|
|
9
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
|
|
10
|
+
|
|
11
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { emptyMap() }
|
|
12
|
+
|
|
13
|
+
companion object {
|
|
14
|
+
init {
|
|
15
|
+
NitroCompassOnLoad.initializeNative()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { withInfoPlist } = require('@expo/config-plugins')
|
|
2
|
+
|
|
3
|
+
const DEFAULT_LOCATION_DESCRIPTION =
|
|
4
|
+
'$(PRODUCT_NAME) uses your heading to power the compass.'
|
|
5
|
+
|
|
6
|
+
const withNitroCompass = (config, props = {}) => {
|
|
7
|
+
return withInfoPlist(config, (cfg) => {
|
|
8
|
+
cfg.modResults.NSLocationWhenInUseUsageDescription =
|
|
9
|
+
props.locationWhenInUsePermission ||
|
|
10
|
+
cfg.modResults.NSLocationWhenInUseUsageDescription ||
|
|
11
|
+
DEFAULT_LOCATION_DESCRIPTION
|
|
12
|
+
return cfg
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = withNitroCompass
|