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