@blife/rn-step-counter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.kr.md +159 -0
  3. package/README.md +158 -0
  4. package/StepCounter.podspec +22 -0
  5. package/android/README.md +65 -0
  6. package/android/build.gradle +66 -0
  7. package/android/gradle.properties +5 -0
  8. package/android/src/main/AndroidManifest.xml +15 -0
  9. package/android/src/main/java/com/stepcounter/StepCounterModule.kt +190 -0
  10. package/android/src/main/java/com/stepcounter/StepCounterPackage.kt +53 -0
  11. package/android/src/main/java/com/stepcounter/services/AccelerometerService.kt +147 -0
  12. package/android/src/main/java/com/stepcounter/services/SensorListenService.kt +305 -0
  13. package/android/src/main/java/com/stepcounter/services/StepCounterService.kt +79 -0
  14. package/android/src/main/java/com/stepcounter/utils/AndroidVersionHelper.kt +33 -0
  15. package/android/src/main/java/com/stepcounter/utils/SensorFusionMath.kt +104 -0
  16. package/ios/SOMotionDetecter.h +44 -0
  17. package/ios/SOMotionDetecter.m +91 -0
  18. package/ios/StepCounter.h +7 -0
  19. package/ios/StepCounter.mm +264 -0
  20. package/lib/module/NativeStepCounter.js +21 -0
  21. package/lib/module/NativeStepCounter.js.map +1 -0
  22. package/lib/module/index.js +194 -0
  23. package/lib/module/index.js.map +1 -0
  24. package/lib/module/package.json +1 -0
  25. package/lib/typescript/jest.setup.d.ts +2 -0
  26. package/lib/typescript/jest.setup.d.ts.map +1 -0
  27. package/lib/typescript/package.json +1 -0
  28. package/lib/typescript/src/NativeStepCounter.d.ts +60 -0
  29. package/lib/typescript/src/NativeStepCounter.d.ts.map +1 -0
  30. package/lib/typescript/src/index.d.ts +80 -0
  31. package/lib/typescript/src/index.d.ts.map +1 -0
  32. package/package.json +138 -0
  33. package/src/NativeStepCounter.ts +62 -0
  34. package/src/index.tsx +263 -0
@@ -0,0 +1,190 @@
1
+ package com.stepcounter
2
+
3
+ import android.content.Context
4
+ import android.hardware.SensorManager
5
+ import android.os.Build.VERSION_CODES
6
+ import android.util.Log
7
+ import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
8
+ import androidx.core.content.PermissionChecker.checkSelfPermission
9
+ import com.facebook.react.bridge.Arguments
10
+ import com.facebook.react.bridge.Promise
11
+ import com.facebook.react.bridge.ReactApplicationContext
12
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
13
+ import com.facebook.react.bridge.ReactMethod
14
+ import com.facebook.react.bridge.WritableMap
15
+ import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
16
+ import com.stepcounter.services.AccelerometerService
17
+ import com.stepcounter.services.SensorListenService
18
+ import com.stepcounter.services.StepCounterService
19
+ import com.stepcounter.utils.AndroidVersionHelper
20
+
21
+ /**
22
+ * This class is the native module for the react-native-step-counter package.
23
+ *
24
+ * It is responsible for the communication between the native and the react-native code.
25
+ * @param reactContext The context of the react-native application
26
+ * @property appContext The context of the react-native application from [context][com.facebook.react.bridge.ReactApplicationContext]
27
+ * @property sensorManager The sensor manager that is responsible for the sensor
28
+ * @property stepCounterListener The service that is responsible for the step counter sensor
29
+ * @constructor Creates a new StepCounterModule implements StepCounterSpec
30
+ * @see ReactContextBaseJavaModule
31
+ * @see ReactApplicationContext
32
+ * @see NativeStepCounterSpec
33
+ */
34
+ class StepCounterModule(
35
+ reactContext: ReactApplicationContext,
36
+ ) : NativeStepCounterSpec(reactContext) {
37
+ companion object {
38
+ const val NAME: String = NativeStepCounterSpec.NAME
39
+ private val TAG_NAME: String = StepCounterModule::class.java.name
40
+ private const val STEP_COUNTER = "android.permission.ACTIVITY_RECOGNITION"
41
+ }
42
+
43
+ private val appContext: ReactApplicationContext = reactContext
44
+ private var sensorManager: SensorManager =
45
+ reactContext.getSystemService(
46
+ Context.SENSOR_SERVICE,
47
+ ) as SensorManager
48
+ private val stepsOK: Boolean
49
+ get() = checkSelfPermission(appContext, STEP_COUNTER) == PERMISSION_GRANTED
50
+ private val accelOK: Boolean
51
+ get() = AndroidVersionHelper.isHardwareAccelerometerEnabled(appContext)
52
+ private val supported: Boolean
53
+ get() = AndroidVersionHelper.isHardwareStepCounterEnabled(appContext)
54
+ private val walkingStatus: Boolean
55
+ get() = stepCounterListener !== null
56
+
57
+ /**
58
+ * gets the step counter listener
59
+ * @return the step counter listener
60
+ * @see SensorListenService
61
+ * @see StepCounterService
62
+ * @see AccelerometerService
63
+ * @see checkSelfPermission
64
+ * @see PERMISSION_GRANTED
65
+ */
66
+ private var stepCounterListener: SensorListenService? = null
67
+
68
+ /**
69
+ * The method that is called when the module is initialized.
70
+ * It checks the permission and the availability for the step counter sensor and initializes the step counter service.
71
+ */
72
+ init {
73
+ stepCounterListener =
74
+ if (stepsOK) {
75
+ StepCounterService(this, sensorManager)
76
+ } else {
77
+ AccelerometerService(this, sensorManager)
78
+ }
79
+ appContext.addLifecycleEventListener(stepCounterListener)
80
+ }
81
+
82
+ /**
83
+ * The method ask if the step counter sensor is supported.
84
+ * @param promise the promise that is used to return the result to the react-native code
85
+ * @see Promise.resolve
86
+ * @see VERSION_CODES.ECLAIR
87
+ * @see VERSION_CODES.KITKAT
88
+ * @see WritableMap
89
+ */
90
+ @ReactMethod
91
+ override fun isStepCountingSupported(promise: Promise) {
92
+ Log.d(TAG_NAME, "hardware_step_counter? $supported")
93
+ Log.d(TAG_NAME, "step_counter granted? $stepsOK")
94
+ Log.d(TAG_NAME, "accelerometer granted? $accelOK")
95
+ sendDeviceEvent("stepDetected", walkingStatus)
96
+ promise.resolve(
97
+ Arguments.createMap().apply {
98
+ putBoolean("supported", supported)
99
+ putBoolean("granted", stepsOK || accelOK)
100
+ putBoolean("working", walkingStatus)
101
+ },
102
+ )
103
+ }
104
+
105
+ /**
106
+ * Start the step counter sensor.
107
+ * @param from the number of steps to start from
108
+ */
109
+ @ReactMethod
110
+ override fun startStepCounterUpdate(from: Double) {
111
+ val sessionStartMillis = normalizeStartTimestampToMillis(from)
112
+ stepCounterListener = stepCounterListener ?: if (stepsOK) {
113
+ StepCounterService(this, sensorManager)
114
+ } else {
115
+ AccelerometerService(this, sensorManager)
116
+ }
117
+ Log.d(TAG_NAME, "startStepCounterUpdate")
118
+ stepCounterListener!!.startService(sessionStartMillis)
119
+ }
120
+
121
+ /**
122
+ * Stop the step counter sensor.
123
+ * @return Nothing.
124
+ */
125
+ @ReactMethod
126
+ override fun stopStepCounterUpdate() {
127
+ Log.d(TAG_NAME, "stopStepCounterUpdate")
128
+ stepCounterListener!!.stopService()
129
+ }
130
+
131
+ /**
132
+ * Keep: Required for RN built in Event Emitter Support.
133
+ * @param eventName the name of the event. usually "stepCounterUpdate".
134
+ */
135
+ @ReactMethod
136
+ override fun addListener(eventName: String) {
137
+ }
138
+
139
+ /**
140
+ * Keep: Required for RN built in Event Emitter Support.
141
+ * @param count the number of listeners to remove.
142
+ * not implemented.
143
+ */
144
+ @ReactMethod
145
+ override fun removeListeners(count: Double) {
146
+ }
147
+
148
+ /**
149
+ * StepCounterPackage requires this property for the module.
150
+ * @return the name of the module. usually "StepCounter".
151
+ */
152
+ override fun getName(): String = NAME
153
+
154
+ /**
155
+ * Send the step counter update event to the react-native code.
156
+ * @param eventPayload the object that contains information about the step counter update.
157
+ * @return Nothing.
158
+ * @see WritableMap
159
+ * @see RCTDeviceEventEmitter
160
+ * @see com.facebook.react.modules.core.DeviceEventManagerModule
161
+ * @throws RuntimeException if the event emitter is not initialized.
162
+ */
163
+ fun sendDeviceEvent(
164
+ eventType: String,
165
+ eventPayload: Any,
166
+ ) {
167
+ try {
168
+ appContext
169
+ .getJSModule(RCTDeviceEventEmitter::class.java)
170
+ .emit(eventName = "$NAME.$eventType", data = eventPayload)
171
+ } catch (e: RuntimeException) {
172
+ e.message?.let { Log.e(TAG_NAME, it) }
173
+ Log.e(TAG_NAME, eventType, e)
174
+ }
175
+ }
176
+
177
+ /**
178
+ * JS passes ms; accept seconds too for backward-compat.
179
+ */
180
+ private fun normalizeStartTimestampToMillis(from: Double): Long {
181
+ if (!from.isFinite() || from <= 0) {
182
+ return System.currentTimeMillis()
183
+ }
184
+ return if (from >= 1_000_000_000_000.0) {
185
+ from.toLong()
186
+ } else {
187
+ (from * 1000).toLong()
188
+ }
189
+ }
190
+ }
@@ -0,0 +1,53 @@
1
+ package com.stepcounter
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.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+
9
+ /**
10
+ * This class is responsible for the creation of the ReactNative package.
11
+ * @see com.facebook.react.ReactPackage
12
+ * @see BaseReactPackage
13
+ * @see ReactApplicationContext
14
+ * @see ReactModuleInfo
15
+ * @see ReactModuleInfoProvider
16
+ */
17
+ class StepCounterPackage : BaseReactPackage() {
18
+ /**
19
+ * This method is responsible for the creation of the ReactNative module.
20
+ * @param name The name of the module
21
+ * @param reactContext The context of the react-native application
22
+ * @return [com.facebook.react.module.model.ReactModuleInfo] ]The ReactNative module
23
+ * @see NativeModule
24
+ * @see ReactApplicationContext
25
+ * @see StepCounterModule
26
+ * @see StepCounterModule.NAME
27
+ */
28
+ override fun getModule(
29
+ name: String,
30
+ reactContext: ReactApplicationContext,
31
+ ): NativeModule? = if (name == StepCounterModule.NAME) StepCounterModule(reactContext) else null
32
+
33
+ /**
34
+ * This method is responsible for the creation of the ReactNative module info provider.
35
+ * @return The ReactNative module info provider
36
+ * @see ReactModuleInfoProvider
37
+ * @see ReactModuleInfo
38
+ */
39
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
40
+ ReactModuleInfoProvider {
41
+ val moduleInfo: MutableMap<String, ReactModuleInfo> = HashMap()
42
+ moduleInfo[StepCounterModule.NAME] =
43
+ ReactModuleInfo(
44
+ name = StepCounterModule.NAME,
45
+ className = StepCounterModule.NAME,
46
+ canOverrideExistingModule = false,
47
+ needsEagerInit = false,
48
+ isCxxModule = false,
49
+ isTurboModule = true,
50
+ )
51
+ moduleInfo
52
+ }
53
+ }
@@ -0,0 +1,147 @@
1
+ package com.stepcounter.services
2
+
3
+ import android.hardware.Sensor
4
+ import android.hardware.SensorManager
5
+ import android.util.Log
6
+ import com.stepcounter.StepCounterModule
7
+ import com.stepcounter.utils.SensorFusionMath.dot
8
+ import com.stepcounter.utils.SensorFusionMath.norm
9
+ import com.stepcounter.utils.SensorFusionMath.normalize
10
+ import com.stepcounter.utils.SensorFusionMath.sum
11
+ import kotlin.math.min
12
+
13
+ /**
14
+ * This class is responsible for listening to the accelerometer sensor.
15
+ * It is used to count the steps of the user.
16
+ * @constructor Creates a new AccelerometerService
17
+ * @param counterModule The module that is responsible for the communication with the react-native layer
18
+ * @param sensorManager The sensor manager that is responsible for the sensor
19
+ *
20
+ * @property sensorTypeString The type of the sensor as a string "Accelerometer"
21
+ * @property sensorType The type of the sensor
22
+ * @property detectedSensor The sensor that is detected
23
+ * @property currentSteps The current steps
24
+ * @property velocityRingCounter The velocity ring counter
25
+ * @property accelRingCounter The acceleration ring counter
26
+ * @property oldVelocityEstimate The old velocity estimate
27
+ * @property startDate The last step time in milliseconds
28
+ * @property accelRingX The acceleration ring for the x-axis
29
+ * @property accelRingY The acceleration ring for the y-axis
30
+ * @property accelRingZ The acceleration ring for the z-axis
31
+ * @property velocityRing The velocity ring
32
+ *
33
+ * @see SensorListenService
34
+ * @see Sensor
35
+ * @see SensorManager
36
+ * @see StepCounterModule
37
+ * @see SensorManager.SENSOR_DELAY_NORMAL
38
+ * @see Sensor.TYPE_ACCELEROMETER
39
+ */
40
+ class AccelerometerService(
41
+ counterModule: StepCounterModule,
42
+ sensorManager: SensorManager,
43
+ ) : SensorListenService(counterModule, sensorManager) {
44
+ override val sensorTypeString = "Accelerometer"
45
+ override val sensorType = Sensor.TYPE_ACCELEROMETER
46
+ override val detectedSensor: Sensor? = sensorManager.getDefaultSensor(sensorType)
47
+ override var currentSteps: Double = 0.0
48
+ private var velocityRingCounter: Int = 0
49
+ private var accelRingCounter: Int = 0
50
+ private var oldVelocityEstimate: Float = 0f
51
+ private var lastStepTimeNs: Long = 0
52
+
53
+ // We want to keep a history of values to do a rolling average of the current
54
+ private val accelRingX = FloatArray(ACCEL_RING_SIZE)
55
+ private val accelRingY = FloatArray(ACCEL_RING_SIZE)
56
+ private val accelRingZ = FloatArray(ACCEL_RING_SIZE)
57
+
58
+ // We want to keep a history of values to do a rolling average of the current
59
+ private val velocityRing = FloatArray(VELOCITY_RING_SIZE)
60
+
61
+ override fun resetSessionState() {
62
+ currentSteps = 0.0
63
+ velocityRingCounter = 0
64
+ accelRingCounter = 0
65
+ oldVelocityEstimate = 0f
66
+ lastStepTimeNs = 0
67
+ accelRingX.fill(0f)
68
+ accelRingY.fill(0f)
69
+ accelRingZ.fill(0f)
70
+ velocityRing.fill(0f)
71
+ }
72
+
73
+ /**
74
+ * This function is responsible for updating the current steps.
75
+ * All [values][android.hardware.SensorEvent.values] are in SI units (m/s^2)
76
+ *
77
+ * - values[0]: Acceleration minus Gx on the x-axis
78
+ * - values[1]: Acceleration minus Gy on the y-axis
79
+ * - values[2]: Acceleration minus Gz on the z-axis
80
+ *
81
+ * @param eventData array of vector.
82
+ * @return If the current step is just updated
83
+ * @see android.hardware.SensorEvent
84
+ * @see android.hardware.SensorEvent.values
85
+ * @see android.hardware.SensorEvent.timestamp
86
+ */
87
+ override fun updateCurrentSteps(eventData: FloatArray): Boolean {
88
+ val timeNs = System.nanoTime()
89
+ // First step is to update our guess of where the global z vector is.
90
+ accelRingCounter++
91
+ // We keep a rolling average of the last 50 values
92
+ accelRingX[accelRingCounter % ACCEL_RING_SIZE] = eventData[0]
93
+ accelRingY[accelRingCounter % ACCEL_RING_SIZE] = eventData[1]
94
+ accelRingZ[accelRingCounter % ACCEL_RING_SIZE] = eventData[2]
95
+ // Next we'll calculate the average of the last 50 vectors in the ring
96
+ val gravity: FloatArray =
97
+ floatArrayOf(
98
+ sum(accelRingX) / min(accelRingCounter, ACCEL_RING_SIZE),
99
+ sum(accelRingY) / min(accelRingCounter, ACCEL_RING_SIZE),
100
+ sum(accelRingZ) / min(accelRingCounter, ACCEL_RING_SIZE),
101
+ )
102
+ // Next step is to figure out the component of the current acceleration
103
+ // in the direction of world_z and subtract gravity's contribution
104
+ val currentZ: Float = dot(normalize(gravity), eventData) - norm(gravity)
105
+ // Now we just need to update our estimate of the velocity
106
+ velocityRingCounter++
107
+ // We keep a rolling average of the last 10 values
108
+ velocityRing[velocityRingCounter % VELOCITY_RING_SIZE] = currentZ
109
+ // Calculate the average of the last 10 values
110
+ val velocityEstimate: Float = sum(velocityRing)
111
+ // If the velocity estimate is greater than the threshold and the previous
112
+ val isWalkingOrRunning: Boolean =
113
+ STEP_THRESHOLD in oldVelocityEstimate..<velocityEstimate &&
114
+ timeNs - lastStepTimeNs > STEP_DELAY_NS
115
+ if (isWalkingOrRunning) {
116
+ currentSteps = currentSteps.plus(1)
117
+ Log.d(TAG_NAME, "STATUS: $currentSteps steps. TIMESTAMP: $timeNs")
118
+ lastStepTimeNs = timeNs
119
+ }
120
+ oldVelocityEstimate = velocityEstimate
121
+ return isWalkingOrRunning
122
+ }
123
+
124
+ companion object {
125
+ /**
126
+ * The delay between steps in nanoseconds
127
+ */
128
+ private const val STEP_DELAY_NS = 250000000 // 250ms
129
+
130
+ /**
131
+ * The size of the acceleration sensor data ring
132
+ */
133
+ private const val ACCEL_RING_SIZE = 50
134
+
135
+ /**
136
+ * The size of the acceleration's velocity ring
137
+ */
138
+ private const val VELOCITY_RING_SIZE = 10
139
+
140
+ /**
141
+ * The minimum acceleration that is considered a step
142
+ */
143
+ private const val STEP_THRESHOLD = 12f // 4f-16f
144
+
145
+ val TAG_NAME: String = AccelerometerService::class.java.name
146
+ }
147
+ }
@@ -0,0 +1,305 @@
1
+ package com.stepcounter.services
2
+
3
+ import android.hardware.Sensor
4
+ import android.hardware.SensorEvent
5
+ import android.hardware.SensorEventListener
6
+ import android.hardware.SensorManager
7
+ import android.util.Log
8
+ import com.facebook.react.bridge.Arguments
9
+ import com.facebook.react.bridge.LifecycleEventListener
10
+ import com.facebook.react.bridge.WritableMap
11
+ import com.stepcounter.StepCounterModule
12
+
13
+ /**
14
+ * the base class for sensor listen service
15
+ * @param counterModule the step counter module
16
+ * @param sensorManager the sensor manager
17
+ * @see StepCounterModule
18
+ * @see Sensor
19
+ * @see SensorEvent
20
+ * @see <a href="https://developer.android.com/reference/android/hardware/SensorEventListener">SensorEventListener</a>
21
+ * @see <a href="https://developer.android.com/reference/android/hardware/SensorManager">SensorManager</a>
22
+ * @see <a href="https://reactnative.dev/docs/native-modules-android#listening-to-lifecycle-events">LifecycleEventListener: document</a>
23
+ * @see <a href="https://github.com/facebook/react-native/blob/main/ReactAndroid/src/main/java/com/facebook/react/bridge/LifecycleEventListener.java">LifecycleEventListener: original file</a>
24
+ */
25
+ abstract class SensorListenService(
26
+ private val counterModule: StepCounterModule,
27
+ private val sensorManager: SensorManager,
28
+ ) : SensorEventListener,
29
+ LifecycleEventListener {
30
+ /**
31
+ * the accelerometer sensor type
32
+ * [TYPE_ACCELEROMETER][Sensor.TYPE_ACCELEROMETER]: 1<br/>
33
+ *
34
+ * the step counter sensor type
35
+ * [TYPE_STEP_COUNTER][Sensor.TYPE_STEP_COUNTER]: 19<br/>
36
+ */
37
+ abstract val sensorType: Int
38
+
39
+ /**
40
+ * The `rate` of [sensor events][android.hardware.SensorEvent] are
41
+ * delivered at. This is only a hint to the system. Events may be received faster or
42
+ * slower than the specified rate. Usually events are received faster.
43
+ *
44
+ * [samplingPeriodUs] – The desired delay between two consecutive events in microseconds.
45
+ * This is only a hint to the system. Events may be received faster or slower than the specified rate.
46
+ * Typically, events are received faster. Can be one of [SENSOR_DELAY_NORMAL][SensorManager.SENSOR_DELAY_NORMAL], [SENSOR_DELAY_UI][SensorManager.SENSOR_DELAY_UI],
47
+ * [SENSOR_DELAY_GAME][SensorManager.SENSOR_DELAY_GAME], or [SENSOR_DELAY_FASTEST][SensorManager.SENSOR_DELAY_FASTEST]
48
+ * or, the desired delay between events in microseconds. Specifying the delay in microseconds only works
49
+ * from Android 2.3 (API level 9) onwards. For earlier releases, you must use one of the `SENSOR_DELAY_*` constants.
50
+ *
51
+ * @see
52
+ * The following are the constants for the sampling period in microseconds
53
+ * <pre class="prettyprint kotlin">
54
+ * private fun getDelay(rate: Int): Int {
55
+ * return when (rate) {
56
+ * SensorManager.SENSOR_DELAY_FASTEST -> 0
57
+ * SensorManager.SENSOR_DELAY_GAME -> 20000
58
+ * SensorManager.SENSOR_DELAY_UI -> 66667
59
+ * SensorManager.SENSOR_DELAY_NORMAL -> 200000
60
+ * else -> rate
61
+ * }
62
+ * }
63
+ * </pre>
64
+ */
65
+ private val samplingPeriodUs
66
+ get() =
67
+ when (sensorType) {
68
+ Sensor.TYPE_ACCELEROMETER -> SensorManager.SENSOR_DELAY_GAME
69
+ Sensor.TYPE_STEP_COUNTER -> SensorManager.SENSOR_DELAY_NORMAL
70
+ else -> SensorManager.SENSOR_DELAY_UI
71
+ }
72
+
73
+ /**
74
+ * @return if the [sensor][detectedSensor] is
75
+ * [accelerometer][Sensor.TYPE_ACCELEROMETER],
76
+ * "Accelerometer"
77
+ * if it's [stepCounter][Sensor.TYPE_STEP_COUNTER],
78
+ * "Step Counter".
79
+ */
80
+ abstract val sensorTypeString: String
81
+
82
+ /**
83
+ * the detected sensor
84
+ * @see SensorManager.getDefaultSensor
85
+ * @see SensorManager.getSensorList
86
+ * @see Sensor.TYPE_ACCELEROMETER
87
+ * @see Sensor.TYPE_STEP_COUNTER
88
+ */
89
+ abstract val detectedSensor: Sensor?
90
+
91
+ /**
92
+ * the current steps data of the user
93
+ * @see currentSteps
94
+ * @see distance
95
+ * @see endDate
96
+ * @see sensorTypeString
97
+ * @see calories
98
+ * @see WritableMap
99
+ * @see Arguments.createMap
100
+ */
101
+ val stepsParamsMap: WritableMap
102
+ get() =
103
+ Arguments.createMap().apply {
104
+ putNumber("steps", currentSteps)
105
+ putNumber("distance", distance)
106
+ putNumber("startDate", startDate)
107
+ putNumber("endDate", endDate)
108
+ putString("counterType", sensorTypeString)
109
+ putNumber("calories", calories)
110
+ }
111
+
112
+ val stepsSensorInfo: WritableMap
113
+ get() =
114
+ Arguments.createMap().apply {
115
+ putNumber("minDelay", detectedSensor!!.minDelay)
116
+ putNumber("maxDelay", detectedSensor!!.maxDelay)
117
+ putString("name", detectedSensor!!.name)
118
+ putString("vendor", detectedSensor!!.vendor)
119
+ putNumber("power", detectedSensor!!.power)
120
+ putNumber("resolution", detectedSensor!!.resolution)
121
+ putBoolean("wakeUpSensor", detectedSensor!!.isWakeUpSensor)
122
+ putBoolean("additionalInfoSupported", detectedSensor!!.isAdditionalInfoSupported)
123
+ }
124
+
125
+ /**
126
+ * Number of in-database-saved calories.
127
+ * 0.045 is the average calories burned per step.
128
+ * to get more accurate result, you can use this formula:
129
+ * 0.045 * weight * distance,
130
+ * but then you need to get the weight of the user. so it needs some permission.
131
+ */
132
+ private val calories: Double
133
+ get() = currentSteps * 0.045
134
+
135
+ /**
136
+ * Distance of in-database-saved steps
137
+ */
138
+ private val distance: Double
139
+ get() = currentSteps * 0.762
140
+
141
+ /**
142
+ * Number of steps counted since service start
143
+ */
144
+ abstract var currentSteps: Double
145
+
146
+ /**
147
+ * Reset in-memory session state before a new counting session starts.
148
+ */
149
+ protected open fun resetSessionState() {
150
+ }
151
+
152
+ /**
153
+ * Start date of the step counting. UTC milliseconds
154
+ */
155
+ private var startDate: Long = System.currentTimeMillis()
156
+
157
+ /**
158
+ * End date of the step counting. UTC milliseconds
159
+ */
160
+ private val endDate: Long
161
+ get() = System.currentTimeMillis()
162
+
163
+ private val sensorDelay: Int
164
+ get() =
165
+ when (samplingPeriodUs) {
166
+ SensorManager.SENSOR_DELAY_FASTEST -> 0
167
+ SensorManager.SENSOR_DELAY_GAME -> 20000
168
+ SensorManager.SENSOR_DELAY_UI -> 66667
169
+ SensorManager.SENSOR_DELAY_NORMAL -> 200000
170
+ else -> samplingPeriodUs
171
+ }
172
+
173
+ /**
174
+ * this class is not implemented Service class now, but made it work so
175
+ * @see android.content.Context.startService
176
+ * @see android.content.Context.stopService
177
+ * @see SensorManager.registerListener
178
+ */
179
+ fun startService(startDateMillis: Long? = null) {
180
+ startDate =
181
+ startDateMillis?.takeIf { it > 0 } ?: System.currentTimeMillis()
182
+ resetSessionState()
183
+ counterModule.sendDeviceEvent("stepsSensorInfo", stepsSensorInfo)
184
+ Log.d(TAG_NAME, "SensorManager.stepsSensorInfo: $stepsSensorInfo")
185
+ sensorManager.registerListener(this, detectedSensor, samplingPeriodUs)
186
+ }
187
+
188
+ /**
189
+ * this class is not implemented Service class now, but made it work so
190
+ * @see android.content.Context.startService
191
+ * @see android.content.Context.stopService
192
+ * @see SensorManager.unregisterListener
193
+ */
194
+ fun stopService() {
195
+ Log.d(TAG_NAME, "SensorListenService.stopService")
196
+ sensorManager.unregisterListener(this)
197
+ }
198
+
199
+ /**
200
+ * Called when there is a new sensor event. Note that "on changed"
201
+ * is somewhat of a misnomer, as this will also be called if we have a
202
+ * new reading from a sensor with the exact same sensor values (but a
203
+ * newer timestamp).
204
+ * See [SensorManager][android.hardware.SensorManager]
205
+ * for details on possible sensor types.
206
+ * See also [SensorEvent][android.hardware.SensorEvent].
207
+ *
208
+ * **NOTE1:** The application doesn't own the [event][android.hardware.SensorEvent]
209
+ * object passed as a parameter and therefore cannot hold on to it.
210
+ * The object may be part of an internal pool and may be reused by
211
+ * the framework. If you need to hold on to the event, you must make a copy.
212
+ *
213
+ * **NOTE2:** Since the timestamp delivered from JavaScript is based on milliseconds,
214
+ * mistakes can occur if the timestamp of sensor events recorded every moment in nanoseconds is delivered as it is.
215
+ * Thus, we convert timestamp (nanoseconds recorded in sensor events) using [toMillis][java.util.concurrent.TimeUnit.toMillis] method,
216
+ * or call [currentTimeInMillis][System.currentTimeMillis] method according to the function execution time.
217
+ *
218
+ * @param event the [SensorEvent][android.hardware.SensorEvent].
219
+ * @see <a href="https://developer.android.com/reference/android/hardware/SensorEvent">Hardware Activity Sensors</a>
220
+ * @see <a href="https://developer.android.com/reference/android/hardware/SensorEvent#sensor.type_accelerometer:">Accelerometer Sensor Event</a>
221
+ * @see <a href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_STEP_COUNTER">Step Counter Sensor Event</a>
222
+ * @see <a href="https://developer.android.com/reference/android/hardware/Sensor#REPORTING_MODE_ON_CHANGE">Reporting Mode On Change</a>
223
+ */
224
+ override fun onSensorChanged(event: SensorEvent?) {
225
+ if (event?.sensor == null ||
226
+ event.sensor != detectedSensor ||
227
+ event.sensor.type != sensorType ||
228
+ detectedSensor?.type != event.sensor.type
229
+ ) {
230
+ return
231
+ }
232
+ if (updateCurrentSteps(event.values)) {
233
+ counterModule.sendDeviceEvent("stepCounterUpdate", stepsParamsMap)
234
+ }
235
+ }
236
+
237
+ /**
238
+ * abstract method to update the current steps
239
+ * implemented in [StepCounterService] and [AccelerometerService]
240
+ * with different motion sensor handling algorithm.
241
+ * @param eventData the detected vector of sensor event
242
+ * @return if the current steps is updated, return true, otherwise return false
243
+ */
244
+ abstract fun updateCurrentSteps(eventData: FloatArray): Boolean
245
+
246
+ /**
247
+ * Called when the accuracy of the registered sensor has changed.
248
+ *
249
+ * Unlike onSensorChanged(), this is only called when this accuracy value changes.
250
+ * See the SENSOR_STATUS_* constants in
251
+ * [SensorManager][android.hardware.SensorManager] for details.
252
+ *
253
+ * @param sensor The [Sensor][android.hardware.Sensor] that has accuracy changed.
254
+ * @param accuracy The new accuracy of this sensor, one of
255
+ * `SensorManager.SENSOR_STATUS_*`
256
+ */
257
+ override fun onAccuracyChanged(
258
+ sensor: Sensor?,
259
+ accuracy: Int,
260
+ ) {
261
+ Log.d(TAG_NAME, "onAccuracyChanged.accuracy $accuracy")
262
+ Log.d(TAG_NAME, "onAccuracyChanged.sensor: $sensor")
263
+ }
264
+
265
+ /**
266
+ * Called either when the host activity receives a resume event or
267
+ * if the native module that implements this is initialized while the host activity is already
268
+ * resumed. Always called for the most current activity.
269
+ * @see Activity.onResume
270
+ */
271
+ override fun onHostResume() {
272
+ }
273
+
274
+ /**
275
+ * Called when host activity receives pause event.
276
+ * Always called for the most current activity.
277
+ * @see Activity.onPause
278
+ */
279
+ override fun onHostPause() {
280
+ }
281
+
282
+ /**
283
+ * Called when host activity receives destroy event.
284
+ * Only called for the last React activity to be destroyed.
285
+ * @see Activity.onDestroy
286
+ */
287
+ override fun onHostDestroy() {
288
+ this.stopService()
289
+ }
290
+
291
+ companion object {
292
+ val TAG_NAME: String = SensorListenService::class.java.name
293
+ }
294
+ }
295
+
296
+ private fun WritableMap.putNumber(
297
+ key: String,
298
+ number: Number,
299
+ ) {
300
+ when (number) {
301
+ is Double -> putDouble(key, number)
302
+ is Int -> putInt(key, number)
303
+ else -> putDouble(key, number.toDouble())
304
+ }
305
+ }