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.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/NitroCompass.podspec +31 -0
  3. package/README.md +206 -0
  4. package/android/CMakeLists.txt +32 -0
  5. package/android/build.gradle +148 -0
  6. package/android/fix-prefab.gradle +51 -0
  7. package/android/gradle.properties +5 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +9 -0
  10. package/android/src/main/java/com/margelo/nitro/nitrocompass/HybridNitroCompass.kt +481 -0
  11. package/android/src/main/java/com/margelo/nitro/nitrocompass/NitroCompassPackage.kt +18 -0
  12. package/app.plugin.js +16 -0
  13. package/ios/Bridge.h +8 -0
  14. package/ios/HybridNitroCompass.swift +473 -0
  15. package/lib/commonjs/hook.js +69 -0
  16. package/lib/commonjs/hook.js.map +1 -0
  17. package/lib/commonjs/index.js +39 -0
  18. package/lib/commonjs/index.js.map +1 -0
  19. package/lib/commonjs/multiplex.js +109 -0
  20. package/lib/commonjs/multiplex.js.map +1 -0
  21. package/lib/commonjs/native.js +9 -0
  22. package/lib/commonjs/native.js.map +1 -0
  23. package/lib/commonjs/package.json +1 -0
  24. package/lib/commonjs/specs/NitroCompass.nitro.js +6 -0
  25. package/lib/commonjs/specs/NitroCompass.nitro.js.map +1 -0
  26. package/lib/module/hook.js +65 -0
  27. package/lib/module/hook.js.map +1 -0
  28. package/lib/module/index.js +6 -0
  29. package/lib/module/index.js.map +1 -0
  30. package/lib/module/multiplex.js +103 -0
  31. package/lib/module/multiplex.js.map +1 -0
  32. package/lib/module/native.js +5 -0
  33. package/lib/module/native.js.map +1 -0
  34. package/lib/module/specs/NitroCompass.nitro.js +4 -0
  35. package/lib/module/specs/NitroCompass.nitro.js.map +1 -0
  36. package/lib/typescript/src/hook.d.ts +49 -0
  37. package/lib/typescript/src/hook.d.ts.map +1 -0
  38. package/lib/typescript/src/index.d.ts +8 -0
  39. package/lib/typescript/src/index.d.ts.map +1 -0
  40. package/lib/typescript/src/multiplex.d.ts +38 -0
  41. package/lib/typescript/src/multiplex.d.ts.map +1 -0
  42. package/lib/typescript/src/native.d.ts +3 -0
  43. package/lib/typescript/src/native.d.ts.map +1 -0
  44. package/lib/typescript/src/specs/NitroCompass.nitro.d.ts +176 -0
  45. package/lib/typescript/src/specs/NitroCompass.nitro.d.ts.map +1 -0
  46. package/nitro.json +30 -0
  47. package/nitrogen/generated/.gitattributes +1 -0
  48. package/nitrogen/generated/android/NitroCompass+autolinking.cmake +81 -0
  49. package/nitrogen/generated/android/NitroCompass+autolinking.gradle +27 -0
  50. package/nitrogen/generated/android/NitroCompassOnLoad.cpp +60 -0
  51. package/nitrogen/generated/android/NitroCompassOnLoad.hpp +34 -0
  52. package/nitrogen/generated/android/c++/JAccuracyQuality.hpp +64 -0
  53. package/nitrogen/generated/android/c++/JCompassSample.hpp +61 -0
  54. package/nitrogen/generated/android/c++/JFunc_void_AccuracyQuality.hpp +77 -0
  55. package/nitrogen/generated/android/c++/JFunc_void_CompassSample.hpp +77 -0
  56. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
  57. package/nitrogen/generated/android/c++/JHybridNitroCompassSpec.cpp +143 -0
  58. package/nitrogen/generated/android/c++/JHybridNitroCompassSpec.hpp +75 -0
  59. package/nitrogen/generated/android/c++/JPermissionStatus.hpp +61 -0
  60. package/nitrogen/generated/android/c++/JSensorDiagnostics.hpp +58 -0
  61. package/nitrogen/generated/android/c++/JSensorKind.hpp +61 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/AccuracyQuality.kt +25 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/CompassSample.kt +56 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/Func_void_AccuracyQuality.kt +80 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/Func_void_CompassSample.kt +80 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/Func_void_bool.kt +80 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/HybridNitroCompassSpec.kt +118 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/NitroCompassOnLoad.kt +35 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/PermissionStatus.kt +24 -0
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/SensorDiagnostics.kt +51 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/SensorKind.kt +24 -0
  72. package/nitrogen/generated/ios/NitroCompass+autolinking.rb +62 -0
  73. package/nitrogen/generated/ios/NitroCompass-Swift-Cxx-Bridge.cpp +73 -0
  74. package/nitrogen/generated/ios/NitroCompass-Swift-Cxx-Bridge.hpp +267 -0
  75. package/nitrogen/generated/ios/NitroCompass-Swift-Cxx-Umbrella.hpp +61 -0
  76. package/nitrogen/generated/ios/NitroCompassAutolinking.mm +33 -0
  77. package/nitrogen/generated/ios/NitroCompassAutolinking.swift +26 -0
  78. package/nitrogen/generated/ios/c++/HybridNitroCompassSpecSwift.cpp +11 -0
  79. package/nitrogen/generated/ios/c++/HybridNitroCompassSpecSwift.hpp +180 -0
  80. package/nitrogen/generated/ios/swift/AccuracyQuality.swift +48 -0
  81. package/nitrogen/generated/ios/swift/CompassSample.swift +34 -0
  82. package/nitrogen/generated/ios/swift/Func_void_AccuracyQuality.swift +46 -0
  83. package/nitrogen/generated/ios/swift/Func_void_CompassSample.swift +46 -0
  84. package/nitrogen/generated/ios/swift/Func_void_PermissionStatus.swift +46 -0
  85. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  86. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  87. package/nitrogen/generated/ios/swift/HybridNitroCompassSpec.swift +67 -0
  88. package/nitrogen/generated/ios/swift/HybridNitroCompassSpec_cxx.swift +309 -0
  89. package/nitrogen/generated/ios/swift/PermissionStatus.swift +44 -0
  90. package/nitrogen/generated/ios/swift/SensorDiagnostics.swift +29 -0
  91. package/nitrogen/generated/ios/swift/SensorKind.swift +44 -0
  92. package/nitrogen/generated/shared/c++/AccuracyQuality.hpp +84 -0
  93. package/nitrogen/generated/shared/c++/CompassSample.hpp +87 -0
  94. package/nitrogen/generated/shared/c++/HybridNitroCompassSpec.cpp +33 -0
  95. package/nitrogen/generated/shared/c++/HybridNitroCompassSpec.hpp +87 -0
  96. package/nitrogen/generated/shared/c++/PermissionStatus.hpp +80 -0
  97. package/nitrogen/generated/shared/c++/SensorDiagnostics.hpp +84 -0
  98. package/nitrogen/generated/shared/c++/SensorKind.hpp +80 -0
  99. package/package.json +136 -0
  100. package/react-native.config.js +11 -0
  101. package/src/hook.ts +118 -0
  102. package/src/index.ts +28 -0
  103. package/src/multiplex.ts +117 -0
  104. package/src/native.ts +5 -0
  105. 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
package/ios/Bridge.h ADDED
@@ -0,0 +1,8 @@
1
+ //
2
+ // Bridge.h
3
+ // react-native-nitro-compass
4
+ //
5
+ // Created by Omar Sukarieh on ٦‏/٥‏/٢٠٢٦
6
+ //
7
+
8
+ #pragma once