1 package host.exp.exponent.utils
2 
3 import android.hardware.Sensor
4 import android.hardware.SensorEvent
5 import android.hardware.SensorEventListener
6 import android.hardware.SensorManager
7 import java.util.concurrent.TimeUnit
8 import kotlin.math.abs
9 
10 // Number of nanoseconds that must elapse before we start detecting next gesture.
11 private val MIN_TIME_AFTER_SHAKE_NS = TimeUnit.NANOSECONDS.convert(600, TimeUnit.MILLISECONDS)
12 
13 // Required force to constitute a rage shake. Need to multiply gravity by 1.33 because a rage
14 // shake in one direction should have more force than just the magnitude of free fall.
15 private const val REQUIRED_FORCE = SensorManager.GRAVITY_EARTH * 1.33f
16 
17 /**
18  * Listens for the user shaking their phone. Allocation-less once it starts listening.
19  */
20 class ShakeDetector(private val shakeListener: () -> Unit) : SensorEventListener {
21   private var accelerationX: Float = 0.toFloat()
22   private var accelerationY: Float = 0.toFloat()
23   private var accelerationZ: Float = 0.toFloat()
24 
25   private var sensorManager: SensorManager? = null
26   private var numShakes: Int = 0
27 
28   private var lastDispatchedShakeTimestamp: Long = 0
29 
30   var minRecordedShakes: Int = 3
31 
32   //region publics
33 
34   /**
35    * Start listening for shakes.
36    */
startnull37   fun start(manager: SensorManager) {
38     val accelerometer = manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
39 
40     if (accelerometer != null) {
41       sensorManager = manager
42       manager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_UI)
43       lastDispatchedShakeTimestamp = 0
44       reset()
45     }
46   }
47 
48   /**
49    * Stop listening for shakes.
50    */
stopnull51   fun stop() {
52     sensorManager?.unregisterListener(this)
53     sensorManager = null
54   }
55 
56   //endregion publics
57   //region SensorEventListener
58 
onSensorChangednull59   override fun onSensorChanged(sensorEvent: SensorEvent) {
60     if (sensorEvent.timestamp - lastDispatchedShakeTimestamp < MIN_TIME_AFTER_SHAKE_NS) {
61       return
62     }
63 
64     val ax = sensorEvent.values[0]
65     val ay = sensorEvent.values[1]
66     val az = sensorEvent.values[2] - SensorManager.GRAVITY_EARTH
67 
68     if (atLeastRequiredForce(ax) && ax * accelerationX <= 0) {
69       numShakes++
70       accelerationX = ax
71     } else if (atLeastRequiredForce(ay) && ay * accelerationY <= 0) {
72       numShakes++
73       accelerationY = ay
74     } else if (atLeastRequiredForce(az) && az * accelerationZ <= 0) {
75       numShakes++
76       accelerationZ = az
77     }
78     if (numShakes >= minRecordedShakes) {
79       reset()
80       shakeListener.invoke()
81       lastDispatchedShakeTimestamp = sensorEvent.timestamp
82     }
83   }
84 
onAccuracyChangednull85   override fun onAccuracyChanged(sensor: Sensor, i: Int) {}
86 
87   //endregion SensorEventListener
88   //region internals
89 
90   /** Reset all variables used to keep track of number of shakes recorded.  */
resetnull91   private fun reset() {
92     numShakes = 0
93     accelerationX = 0f
94     accelerationY = 0f
95     accelerationZ = 0f
96   }
97 
98   /**
99    * Determine if acceleration applied to sensor is large enough to count as a rage shake.
100    *
101    * @param a acceleration in x, y, or z applied to the sensor
102    * @return true if the magnitude of the force exceeds the minimum required amount of force. false
103    * otherwise.
104    */
atLeastRequiredForcenull105   private fun atLeastRequiredForce(a: Float): Boolean {
106     return abs(a) > REQUIRED_FORCE
107   }
108 
109   //endregion internals
110 }
111